Python中鲜为人知的__missing__方法:打破魔术方法独立性的特例
本文深入解析Python中鲜为人知的__missing__魔术方法。该方法仅在字典子类如defaultdict和Counter中定义,用于在键缺失时避免KeyError。文章揭示了__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__()。
这是因为它并非内置类型的必要属性,未在字典基类中预先定义。
若直接从 dict 类型中获取该属性值,会报错属性不存在:AttributeError: type object 'object' has no attribute '__missing__'。
使用 dir() 查看,确实发现该属性不存在:

若从 dict 的父类 object 中查看,结果同样如此。
这是为何?为何在 dict 和 object 中均不存在 __missing__ 属性?
然而,查阅最新官方文档,object 中明明包含此属性:

出处:3. Data model — Python 3.10.1 documentationmissing#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__(),它却选择了妥协。
本文揭示了这一魔术方法的神秘之处,不知你读后有何感想呢?
5、总结
到此这篇关于Python中最神秘的一个魔法函数的文章就介绍到这了,更多相关Python魔法函数内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









