目录

魔术方法介绍系列:__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__方法,在该方法中做我们想做的事。

看一个简单的例子:

1
2
3
4
5
6
7
8
class Counter(dict):
  def __missing__(self, key):
    return 0

c = Counter()
print(c['test'])
c['test'] += 1
print(c['test'])

执行结果;

1
2
0
1

这里实现了一个简单的计数器,默认情况下,任何 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异常。

写一段代码来测试一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyDict(dict):
  def __getitem__(self, key):
    print('calling __getitem__ for key {}'.format(key))
    return super().__getitem__(key)

  def __missing__(self, key):
    print('calling __missing__ for key {}'.format(key))
    return 0

d = MyDict()
d['a'] = 'hello'

print("try to get 'a'")
print("d['a'] ->", d['a'])

print("try to get 'b' through d['b']")
print("d['b'] ->", d['b'])

print("try to get 'b' through get() function")
print("get('b') ->", d.get('b'))

print("try to test 'b' in d")
print("'b' in d ? ->", 'b' in d)

执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try to get 'a'
calling __getitem__ for key a
d['a'] -> hello
try to get 'b' through d['b']
calling __getitem__ for key b
calling __missing__ for key b
d['b'] -> 0
try to get 'b' through get() function
get('b') -> None
try to test 'b' in d
'b' in d ? -> False

这里在自定义的子类中重构了__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__方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class MyDict(dict):
  def __getitem__(self, key):
    print('calling __getitem__ for key {}'.format(key))
    return self.get(key, 0)		# call get() instead of parent class's __getitem__

  def __missing__(self, key):
    print('calling __missing__ for key {}'.format(key))
    return 0

d = MyDict()
d['a'] = 'hello'

print("try to get 'a'")
print("d['a'] ->", d['a'])

print("try to get 'b' through d['b']")
print("d['b'] ->", d['b'])

执行结果:

1
2
3
4
5
6
try to get 'a'
calling __getitem__ for key a
d['a'] -> hello
try to get 'b' through d['b']
calling __getitem__ for key b
d['b'] -> 0

在子类中重构__getitem__时,我们没有调用父类的__getitem__方法,而是通过调用 get() 方法来实现,上一节我介绍过,字典的 get() 方法是不会调用__missing__方法的,所以当 d[key] 操作时即使 key 不存在,也不会调用子类中已实现的__missing__方法。

所以,当你实现一个子类,实现了__missing__方法,你需要注意__getitem__方法是否被子类重构了,如果重构了,它还会不会调用__missing__方法,如果不会,那么你的__missing__方法并不会生效,你需要修改__getitem__的实现,让它会在 key 不存在时调用你的__missing__方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class MyDict(dict):
  def __getitem__(self, key):
    print('calling __getitem__ for key {}'.format(key))
    if key not in self and hasattr(self, '__missing__'):
      return self.__missing__(key)
    else:
      return self.get(key, 0)

  def __missing__(self, key):
    print('calling __missing__ for key {}'.format(key))
    return 0

d = MyDict()
d['a'] = 'hello'

print("try to get 'a'")
print("d['a'] ->", d['a'])

print("try to get 'b' through d['b']")
print("d['b'] ->", d['b'])

执行结果:

1
2
3
4
5
6
7
try to get 'a'
calling __getitem__ for key a
d['a'] -> hello
try to get 'b' through d['b']
calling __getitem__ for key b
calling __missing__ for key b
d['b'] -> 0

这里把前一个例子改动一下,在__getitem__方法内判断是否实现了__missing__,如果是就调用它。从而当 key 不存在时,d[key] 操作能顺利地调用__missing__方法。

 

defaultdict 类

如果我们想让 key 不存在时,d[key] 做我们自定义的事,但又不想另外定义一个子类去实现__missing__方法,我们可以使用collections.defaultdict类。defaultdict其实就是一个实现了__missing__方法的 dict 的子类,它的__missing__方法,会调用用户自定义的一个可调用对象(callable object):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from collections import defaultdict

d = defaultdict(int)
d['a'] = 'hello'

print("try to get 'a'")
print("d['a'] ->", d['a'])

print("try to get 'b' through d['b']")
print("d['b'] ->", d['b'])

执行结果:

1
2
3
4
try to get 'a'
d['a'] -> hello
try to get 'b' through d['b']
d['b'] -> 0

这里的代码和前面的例子类似,区别是直接使用了 Python 的defaultdict,而不是自定义子类。创建defaultdict实例的时候,传入了 int,int 是个类对象(class object),也是一个可调用对象,int() 返回 0。所以通过 d[key] 访问一个不存在的 key 时,返回默认值 0。

我们也可以用一个函数来定义 key 不存在时要做的事情:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from collections import defaultdict

def test():
  print("It is in test function")
  return 0

d = defaultdict(test)
d['a'] = 'hello'

print("try to get 'a'")
print("d['a'] ->", d['a'])

print("try to get 'b' through d['b']")
print("d['b'] ->", d['b'])

执行结果:

1
2
3
4
5
try to get 'a'
d['a'] -> hello
try to get 'b' through d['b']
It is in test function
d['b'] -> 0

实际上,在创建一个defaultdict对象时,会把可调用对象(本例中是 test 函数)赋给defaultdict对象的 default_factory 属性,当 d[key] 访问的 key 不存在时,defaultdict__missing__方法内会判断 default_factory 是否为 None,如果是,直接像普通 dict 对象一样,抛出 KeyError 异常,如果不是,则执行 default_factory() 并把返回值赋给 self[key] 并返回它。所以defaultdict__missing__方法的伪代码如下;

1
2
3
4
if self.default_factory is None: 
	raise KeyError((key,))
self[key] = value = self.default_factory()
return value

 

参考

https://stackoverflow.com/questions/38261126/python-2-missing-method

https://docs.python.org/3.7/library/stdtypes.html#dict


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」