当前位置: 首页 > 后端技术 > Python

Python最神奇的法宝,我想就是它了!

时间:2023-03-26 12:46:56 Python

在上一篇文章中,我有一个核心发现:Python内置类型的特殊方法(包括magic方法和其他方法)都是由C语言独立实现的,没有Python层面的调用关系。不过,文中提到了一个例外:一种非常玄妙的法术。这种方法是如此晦涩和狭隘,以至于我几乎从未关注过它,然而,当事实证明它可能是上述“规律”的唯一例外时,我认为值得再写一篇文章来详细研究它。本文的主要关注点是:(1)\_\_missing\_\_()到底在哪里?(2)\_\_missing\_\_()有什么特别之处?你擅长“活改变人”的魔法吗?(3)\_\_missing\_\_()真的是上述发现的例外吗?如果是,为什么会有这样的特例?1.当从普通字典中取出有价值的\_\_missing\_\_()时,key可能不存在:dd={'name':'PythonCat'}dd.get('age')#结果:Nonedd.get('age',18)#Result:18dd['age']#ErrorKeyErrordd.__getitem__('age')#相当于dd['age']对于get()方法,它有一个returnvalue,当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\_\_()从上面可以看出,\_\_missing\_\_()会在\_\_getitem\_\_()取不到值的时候被调用,但是无意中发现一个细节:\_\_getitem\_\_()取不到值时,不一定调用\_\_missing\_\_()。这是因为它不是内置类型的必需属性,也没有在字典基类中预定义。如果直接从dict类型中取属性值,会报属性不存在:AttributeError:typeobject'object'hasnoattribute'__missing__'。用dir()查看,发现这个属性是不存在的:如果从dict的父类,也就是object来看,也会发现同样的结果。这里发生了什么?为什么字典和对象中都没有\_\_missing\_\_属性?然而,查看最新的官方文档,该对象显然包含此属性:来源:https://docs.python.org/3/ref...\_\_missing\_\_#object.\_\_missing\_\_也就是说,理论上会在对象类中预定义\_\_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\_\_()上妥协。本文揭开这个神奇方法的神秘面纱。看完后你有什么感想?欢迎留言讨论。