大家好,我是杰杰,今天给大家介绍一个非常神秘的魔法方法。这种方法是如此晦涩和狭隘,以至于我几乎从未关注过它,然而,当事实证明它可能是上述“规律”的唯一例外时,我认为值得再写一篇文章来详细研究它。本文的主要关注点是:(1)__missing__()到底在哪里?(2)__missing__()有什么特别之处?你擅长“活改变人”的魔法吗?(3)__missing__()真的是上述发现的例外吗?如果是,为什么会有这样的特例?1.当有价值的__missing__()从普通字典取值时,key可能不存在:dd={'name':'PythonCat'}dd.get('age')#Result:Noned.get('age',18)#Result:18dd['age']#ErrorKeyErrordd.__getitem__('age')#相当于dd['age']对于get()方法,它有一个返回值,而且,第二个参数可以在key不存在的时候作为返回内容传入,所以也是可以接受的。但是其他两种写法都会报错。为了解决后两种写法的问题,可以使用__missing__()魔术方法。现在,假设我们有这样一个请求:从字典中取出某个键对应的值,有值就返回值,没有值就插入键,并给它一个默认值(比如一个空列表)。如果使用原始的dict,实现起来不是很方便,但是Python提供了一个很好用的扩展类collections.defaultdict:如图,当fetch一个不存在的key时,不会再次报KeyError,但是默认存储在字典中。为什么defaultdict可以做到这一点?原因是defaultdict在继承内置类型dict之后,还定义了一个__missing__()方法。当__getitem__取一个不存在的值时,它会调用传入参数的工厂函数(上面的例子调用了list(),创建了一个空列表)。最典型的例子,defaultdict在文档注释中写道:简而言之,__missing__()的主要功能是当key丢失时由__getitem__调用,从而避免KeyError。另一个典型的用法例子是collections.Counter,它也是dict的一个子类。当使用未计数的键时,它返回0:2的计数。神秘的__missing__()由上可见。)会在取不到值的时候调用,但是我无意中发现了一个细节:__getitem__()不一定在取不到值的时候调用__missing__()。这是因为它不是内置类型的必需属性,也没有在字典基类中预定义。如果直接从dict类型中取属性值,会报属性不存在:AttributeError:typeobject'object'hasnoattribute'__missing__'。用dir()查看,发现这个属性是不存在的:如果从dict的父类,也就是object来看,也会发现同样的结果。这里发生了什么?为什么字典和对象中都没有__missing__属性?但是查了最新的官方文档,对象中明明有这个属性:来源:https://docs.python.org/3/ref...也就是说,理论上在对象类中预定义了__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__(),它却选择了妥协。本文揭开这个神奇方法的神秘面纱。看完后你有什么感想?欢迎留言讨论。
