Python中神秘的__missing__()方法:原理、特例与底层实现解析

温以宁
温以宁
发布时间:2026-10-15 | 4人浏览过
原创

本文深入解析Python中鲜为人知的__missing__()魔术方法。该方法仅在字典键缺失时被__getitem__调用,是defaultdict和Counter等内置子类的核心机制。文章揭示了__missing__作为唯一由其他魔术方法调用的特例,其依赖__getitem__触发的底层逻辑,以及Python在保持魔术方法独立性上的妥协。

前言

介绍一个极具隐蔽性的魔术方法。

该方法通常不被重视,应用场景有限,笔者也极少关注。然而,当意识到它可能是前述“定律”的唯一例外时,笔者认为值得专门撰文深入剖析。

本文主要探讨以下问题:

(1) __missing__()方法的本质是什么?

(2) __missing__()有何独特之处?它如何实现“无中生有”的效果?

(3) __missing__()是否确为前文所述发现的例外?若是,其存在理由何在?

1、具有实用价值的__missing__()方法

在普通字典中获取不存在的键时,通常会引发异常:


dd = {'name':'PythonCat'}
dd.get('age')        # 结果:None
dd.get('age', 18)    # 结果:18
dd['age']            # 报错 KeyError
dd.__getitem__('age')  # 等同于 dd['age']

对于get()方法,它具备返回值,且允许传入第二个参数作为键不存在时的默认返回内容,因此这种用法尚可接受。然而,另外两种直接访问方式均会引发报错。

为解决后两种写法的问题,可以使用__missing__()魔术方法。

假设存在如下需求:从字典中获取特定键对应的值,若存在则返回该值;若不存在,则插入该键并赋予默认值(例如空列表)。

若使用原生dict实现较为困难,但Python提供了一个极其实用的扩展类collections.defaultdict:

如图所示,当尝试获取不存在的键时,不再抛出KeyError异常,而是自动将该键存入字典中。

defaultdict为何能实现这一功能?

原因在于defaultdict在继承内置类型dict的同时,还定义了__missing__()方法。当通过__getitem__获取不存在的值时,它会调用传入的工厂函数(上例中为list(),用于创建空列表)。

作为典型示例,defaultdict在文档注释中明确指出:

简而言之,__missing__()的主要作用是在__getitem__遇到缺失键时被调用,从而避免抛出KeyError异常。

另一个典型用例是collections.Counter,它同样是dict的子类,在获取未被统计的键时,返回计数值0:

2、__missing__()的隐蔽特性

由上可知,当__getitem__()无法获取值时会调用__missing__(),但笔者还发现一个细节:__getitem__()在取不到值时,并不总是调用__missing__()。

这是因为__missing__()并非内置类型的必要属性,未在字典基类中预先定义。

若直接从dict类型中获取该属性值,将报错属性不存在:AttributeError: type object 'object' has no attribute '__missing__'。

使用dir()查看,确实发现该属性不存在:

若从dict的父类object中查看,结果相同。

这是为何?为何dict和object中均无__missing__属性?

然而,查阅最新官方文档,object中明确包含此属性:

出处:3. Data model — Python 3.10.1 documentation__missing__#object.__missing__

也就是说,理论上object类应预定义__missing__,文档也证实了这一点,但实际并未定义!文档与现实存在偏差。

因此,当dict的子类(如defaultdict和Counter)定义__missing__()时,该魔术方法实际上仅属于该子类,即它是诞生于子类中的魔术方法。

据此,笔者有一个初步猜想:__getitem__()会判断当前对象是否为dict的子类且是否拥有__missing__(),随后决定是否调用它(若父类中已有该方法,则不会先做判断,而是直接调用)。

笔者在交流群中提出此猜想,有同学很快在CPython源码中找到验证:

这便变得有趣了,在整个Python生态中,恐怕难以再找出第二个仅在内置类型子类上存在的魔术方法。

笔者由此产生联想:这个神出鬼没的__missing__(),宛如一位擅长“大变活人”的魔术师。先让观众透过玻璃看到他(即官方文档),揭开帘子时却发现他不在其中(即内置类型),稍作变换后,他又完好无损地出现(即dict的子类)。

3、受魔法驱动的__missing__()方法

__missing__()的神奇之处,不仅在于其本身能“变魔术”,更在于它需要一股强大的“魔法”来驱动。

笔者发现,原生魔术方法之间相互独立,它们在C语言层面可能有相同的核心逻辑,但在Python语言层面,并不存在调用关系:

魔术方法这种“老死不相往来”的表现,违背了常规代码复用原则,也是导致内置类型子类出现某些奇怪表现的原因。

官方Python宁愿提供新的UserString、UserList、UserDict子类,也不愿复用魔术方法,唯一合理的解释似乎是令魔术方法相互调用的代价过高。

然而,对于特例__missing__(),Python不得不妥协,必须付出这种代价!

__missing__()是魔术方法的“二等公民”,它没有独立的调用入口,只能被动由__getitem__()调用,即__missing__()依赖于__getitem__()。

不同于那些“一等公民”,例如__init__()、__enter__()、__len__()、__eq__()等,它们要么在对象生命周期或执行过程的某个节点被触发,要么由某个内置函数或操作符触发,这些都是相对独立的事件,互不依赖。

__missing__()依赖于__getitem__()才能实现方法调用;而__getitem__()也要依赖__missing__(),才能实现完整功能。

为实现这一点,__getitem__()在解释器代码中开了一个后门,从C语言界面折返回Python界面,去调用那个名为“__missing__”的特定方法。

而这才是真正的“魔法”,迄今为止,__missing__()似乎是唯一享受此等待遇的魔术方法!

4、小结

Python的字典提供了两种取值的内置方法,即__getitem__()和get(),当取值不存在时,它们的处理策略不同:前者会报错KeyError,而后者返回None。

为何Python要提供两种不同的方法?或者更准确地说,为何Python要让这两种方法做出不同的处理?

这可能有一个复杂(也可能很简单)的解释,本文暂不深究。

不过有一点可以确定:即原生dict类型简单粗暴地抛出KeyError的做法存在不足。

为了让字典类型具备更强大的表现(或者说让__getitem__()表现出get()那样的行为),Python允许字典子类定义__missing__(),供__getitem__()查找并调用。

本文梳理了__missing__()的实现原理,揭示出它并非毫不起眼的存在,恰恰相反,它是唯一打破魔术方法间壁垒、支持被其他魔术方法调用的特例!

Python为了维持魔术方法的独立性,煞费苦心地引入了UserString、UserList、UserDict这些派生类,但对于__missing__(),它选择了妥协。

到此这篇关于Python中最神秘__missing__()函数介绍的文章就介绍到这了,更多相关Python __missing__()函数内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表