在 Python 2.2 之前,内置类型(如 list 或 dict)不能子类化。在 Python 2.2 之后,内置类型可以子类化了,但是有个重要的注意事项:内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法。
PyPy 的文档使用简明扼要的语言描述了这个问题,见于“Differences between PyPy and CPython”中“Subclasses of built-in types”一节(http://pypy.readthedocs.io/en/latest/cpython_differences.html#subclasses-of-built-in-types):
至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。基本上,内置类型的方法不会调用子类覆盖的方法。例如,
dict的子类覆盖的__getitem__()方法不会被内置类型的get()方法调用。
示例 12-1 说明了这个问题。
示例 12-1 内置类型
dict的__init__和__update__方法会忽略我们覆盖的__setitem__方法
>>> class DoppelDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2) # ➊
...
>>> dd = DoppelDict(one=1) # ➋
>>> dd
{'one': 1}
>>> dd['two'] = 2 # ➌
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3) # ➍
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}
❶ DoppelDict.__setitem__ 方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。
❷ 继承自 dict 的 __init__ 方法显然忽略了我们覆盖的 __setitem__ 方法:'one' 的值没有重复。
❸ [] 运算符会调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即 [2, 2]。
❹ 继承自 dict 的 update 方法也不使用我们覆盖的 __setitem__ 方法:'three' 的值没有重复。
原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。在这种糟糕的局面中,__missing__ 方法(参见 3.4.2 节)却能按预期方式工作,不过这只是特例。
不只实例内部的调用有这个问题(self.get() 不调用 self.__getitem__()),内置类型的方法调用的其他类的方法,如果被覆盖了,也不会被调用。示例 12-2 是一个例子,改编自 PyPy 文档中的示例(http://pypy.readthedocs.io/en/latest/cpython_differences.html#subclasses-of-built-in-types)。
示例 12-2
dict.update方法会忽略AnswerDict.__getitem__方法
>>> class AnswerDict(dict):
... def __getitem__(self, key): # ➊
... return 42
...
>>> ad = AnswerDict(a='foo') # ➋
>>> ad['a'] # ➌
42
>>> d = {}
>>> d.update(ad) # ➍
>>> d['a'] # ➎
'foo'
>>> d
{'a': 'foo'}
❶ 不管传入什么键,AnswerDict.__getitem__ 方法始终返回 42。
❷ ad 是 AnswerDict 的实例,以 ('a', 'foo') 键值对初始化。
❸ ad['a'] 返回 42,这与预期相符。
❹ d 是 dict 的实例,使用 ad 中的值更新 d。
❺ dict.update 方法忽略了 AnswerDict.__getitem__ 方法。
直接子类化内置类型(如
dict、list或str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承collections模块(http://docs.python.org/3/library/collections.html)中的类,例如UserDict、UserList和UserString,这些类做了特殊设计,因此易于扩展。
如果不子类化 dict,而是子类化 collections.UserDict,示例 12-1 和示例 12-2 中暴露的问题便迎刃而解了。参见示例 12-3。
示例 12-3
DoppelDict2和AnswerDict2能像预期那样使用,因为它们扩展的是UserDict,而不是dict
>>> import collections
>>>
>>> class DoppelDict2(collections.UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2)
...
>>> dd = DoppelDict2(one=1)
>>> dd
{'one': [1, 1]}
>>> dd['two'] = 2
>>> dd
{'two': [2, 2], 'one': [1, 1]}
>>> dd.update(three=3)
>>> dd
{'two': [2, 2], 'three': [3, 3], 'one': [1, 1]}
>>>
>>> class AnswerDict2(collections.UserDict):
... def __getitem__(self, key):
... return 42
...
>>> ad = AnswerDict2(a='foo')
>>> ad['a']
42
>>> d = {}
>>> d.update(ad)
>>> d['a']
42
>>> d
{'a': 42}
为了衡量子类化内置类型所需的额外工作量,我做了个实验,重写了示例 3-8 中的 StrKeyDict 类。原始版继承自 collections.UserDict,而且只实现了三个方法:__missing__、__contains__ 和 __setitem__。在实验中,StrKeyDict 直接子类化 dict,而且也实现了那三个方法,不过根据存储数据的方式稍微做了调整。可是,为了让实验版通过原始版的测试组件,还要实现 __init__、get 和 update 方法,因为继承自 dict 的版本拒绝与覆盖的 __missing__、__contains__ 和 __setitem__ 方法合作。示例 3-8 中那个 UserDict 子类有 16 行代码,而实验的 dict 子类有 37 行代码。2
2如果好奇,实验版在本书代码仓库(https://github.com/fluentpython/example-code)里的 strkeydict_ dictsub.py 文件中。
综上,本节所述的问题只发生在 C 语言实现的内置类型内部的方法委托上,而且只影响直接继承内置类型的用户自定义类。如果子类化使用 Python 编写的类,如 UserDict 或 MutableMapping,就不会受此影响。3
3顺便说一下,在这方面,PyPy 的行为比 CPython“正确”,不过会导致微小的差异。详情参见“Differences between PyPy and CPython”(http://pypy.readthedocs.io/en/latest/cpython_differences.html#subclasses-of-built-in-types)。
与继承,尤其是多重继承有关的另一个问题是:如果同级别的超类定义了同名属性,Python 如何确定使用哪个?下一节解答。