魔术方法介绍系列:__missing__方法
__missing__ 方法的作用
Python 的官方文档中,是这样描述__missing__
方法的:
If a subclass of dict defines a method __missing__() and key is not present, the d[key] operation calls that method with the key key as argument. The d[key] operation then returns or raises whatever is returned or raised by the __missing__(key) call. No other operations or methods invoke __missing__(). If __missing__() is not defined, KeyError is raised. __missing__() must be a method; it cannot be an instance variable:
英翻中,如果字典的子类中定义了__missing__
方法,当 key 不存在时,d[key] 操作会调用该方法并以 key 作为方法的参数。d[key] 操作返回的值或者抛出的异常就是__missing__
方法返回的值或者抛出的异常。没有其它操作或方法会直接调用__missing__
方法。如果子类中__missing__
方法没有定义,就会抛出 KeyError 异常。__missing__
必须是一个方法,而不是实例变量。
简单地说,当我们用 d[key] 访问字典中的 key 时,如果 key 不存在,默认会抛出KeyError
异常。有时,我们希望它不抛出异常,而是做其它事,比如返回一个默认值。这种情况下,我们可以编写一个子类,继承自 dict,并实现__missing__
方法,在该方法中做我们想做的事。
看一个简单的例子:
|
|
执行结果;
|
|
这里实现了一个简单的计数器,默认情况下,任何 key 的初始值都是0,因为 key 不在 Counter 对象中,访问它们的值会调用__missing__
方法,从而得到0。这个例子其实是 Python 的 collections.Counter 的部分实现。
get() 方法和 in 运算符会调用 __missing__ 吗 ?
字典的 get() 方法和 in 运算符,和 d[key] 类似,同样也是做和 key 相关的事。get(key) 获取 key 对应的值,in 运算符判断 key 是否在字典中。那么,当 key 不存在的时候,这两个操作会触发__missing__
方法吗?
要知道答案,先要了解 d[key]、get() 和 in 运算符背后是如何实现的。
执行 d[key] 的时候,实际会调用字典的__getitem__
方法;执行 in 运算符判断 key 是否在字典中时,会调用字典的__contains__
方法;而 get() 方法是用 C 语言实现的高效查询。
__missing__
方法只会被__getitem__
方法调用,所以__missing__
方法只会影响 d[key] 操作,而不会影响字典的 get() 方法和 in 运算符。
__getitem__
方法的实现中,当 key 不存在,在抛出 KeyError 异常之前,会先检查该类有没有定义__missing__
方法,如果有,就调用它,而不抛出 KeyError 异常。默认情况下,dict 类没有定义该方法,但是它知道有这么一个东西存在。也就是说,如果有一个类继承了 dict,然后该类实现了__missing__
方法,那么在__getitem__()
方法找不到 key 的时候,就会自动调用它,而不是抛出KeyError
异常。
写一段代码来测试一下:
|
|
执行结果:
|
|
这里在自定义的子类中重构了__getitem__
和__missing__
方法,打印了一些信息用于调试,__getitem__
逻辑没变,__missing__
方法返回 0。‘a’ 在字典中,所以 d[‘a’] 操作调用__getitem__
方法时直接返回它的值;‘b’ 不在字典中,所以 d[‘b’] 调用__getitem__
方法时会调用__missing__
方法,该方法返回 0 作为 d[‘b’] 的返回值;接下来,尝试用 get(‘b’) 获取不存在的键时,不会调用__missing__
方法,所以按照 get() 方法的默认行为,返回 None;同样,in 操作触发的__contains__
方法不会调用__missing__
方法,所以 ’b‘ in d 也按照默认行为,返回 False。
易犯的错误
既然__missing__
是被__getitem__
调用的,那么当用户在自定义的子类中重构__getitem__
时,如果在自己的实现中没有调用__missing__
方法,那么__missing__
的作用将不复存在。
来看一个例子。把上一个例子中的代码稍做修改,重新实现__getitem__
方法:
|
|
执行结果:
|
|
在子类中重构__getitem__
时,我们没有调用父类的__getitem__
方法,而是通过调用 get() 方法来实现,上一节我介绍过,字典的 get() 方法是不会调用__missing__
方法的,所以当 d[key] 操作时即使 key 不存在,也不会调用子类中已实现的__missing__
方法。
所以,当你实现一个子类,实现了__missing__
方法,你需要注意__getitem__
方法是否被子类重构了,如果重构了,它还会不会调用__missing__
方法,如果不会,那么你的__missing__
方法并不会生效,你需要修改__getitem__
的实现,让它会在 key 不存在时调用你的__missing__
方法:
|
|
执行结果:
|
|
这里把前一个例子改动一下,在__getitem__
方法内判断是否实现了__missing__
,如果是就调用它。从而当 key 不存在时,d[key] 操作能顺利地调用__missing__
方法。
defaultdict 类
如果我们想让 key 不存在时,d[key] 做我们自定义的事,但又不想另外定义一个子类去实现__missing__
方法,我们可以使用collections.defaultdict
类。defaultdict
其实就是一个实现了__missing__
方法的 dict 的子类,它的__missing__
方法,会调用用户自定义的一个可调用对象(callable object):
|
|
执行结果:
|
|
这里的代码和前面的例子类似,区别是直接使用了 Python 的defaultdict
,而不是自定义子类。创建defaultdict
实例的时候,传入了 int,int 是个类对象(class object),也是一个可调用对象,int() 返回 0。所以通过 d[key] 访问一个不存在的 key 时,返回默认值 0。
我们也可以用一个函数来定义 key 不存在时要做的事情:
|
|
执行结果:
|
|
实际上,在创建一个defaultdict
对象时,会把可调用对象(本例中是 test 函数)赋给defaultdict
对象的 default_factory 属性,当 d[key] 访问的 key 不存在时,defaultdict
的__missing__
方法内会判断 default_factory 是否为 None,如果是,直接像普通 dict 对象一样,抛出 KeyError 异常,如果不是,则执行 default_factory() 并把返回值赋给 self[key] 并返回它。所以defaultdict
的__missing__
方法的伪代码如下;
|
|
参考
https://stackoverflow.com/questions/38261126/python-2-missing-method
https://docs.python.org/3.7/library/stdtypes.html#dict
「 您的赞赏是激励我创作和分享的最大动力! 」
- 原文链接:https://zhuyinjun.me/2019/python_magic_method_missing/
- 版权声明:本创作采用 CC BY-NC 4.0 国际许可协议,非商业性使用可以转载,但请注明出处(作者、链接),商业性使用请联系作者获得授权。