十二、继承的优缺点
子类化内置类型很麻烦
内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法。
至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖的 __getitem__()
方法不会被内置类型的 get() 方法调用。
__setitem__
__getitem__
方法:只有实例化子类对象,[]
运算符覆盖会生效
直接子类化内置类型(如 dict、list 或 str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承 collections 模块(http://docs.python.org/3/library/collections.html)中的类,例如 UserDict、UserList 和 UserString,这些类做了特殊设计,因此易于扩展。
如果不子类化 dict,而是子类化 collections.UserDict,问题便迎刃而解了。
为
了让实验版通过原始版的测试组件,还要实现 __init__
、get 和 update 方法,因为继承自 dict 的版本拒绝与覆盖的 __missing__
、__contains__
和 __setitem__
方法合作。
多重继承和方法解析顺序
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”。
(左)说明“菱形问题”的 UML 类图;(右)虚线箭头是示例 12-4 使用的方法解析顺序
In [2]: class A:
...: def pint(self):
...: print('ping:',self)
...:
In [7]: class C(A):
...: def pont(self):
...: print('pontCCC:',self)
...:
In [8]: class B(A):
...: def pont(self):
...: print('pontBBB:',self)
...:
In [6]: class D(B,C):
...: def pint(self):
...: super().pint()
...: print('post-pint:', self)
...: def pintpont(self):
...: self.pint()
...: super().pint()
...: self.pont()
...: super().pont()
...: C.pont(self)
...:
In [12]: d = D()
In [13]: d.pont()
pontBBB: <__main__.D object at 0x000001E89548D7F0>
In [14]: C.pont(d) # 显式调用
pontCCC: <__main__.D object at 0x000001E89548D7F0>
In [15]: d.pint()
ping: <__main__.D object at 0x000001E89548D7F0>
post-pint: <__main__.D object at 0x000001E89548D7F0>
In [17]: D.__mro__ # 查找属性和方法 顺序
# 按照方法解析顺序列出各个超类,从当前类一直向上,直到 object 类。
Out[17]: (__main__.D, __main__.B, __main__.C, __main__.A, object)
注意,直接在类上调用实例方法时,必须显式传入 self 参数,因为这样访问的是未绑定方法(unbound method)。
方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果在 diamond.py 文件(见示例 12-4)中把 D 类声明为 class D(C, B):,那么 D 类的__mro__
属性就会不一样:先搜索 C 类,再搜索 B 类。
处理多重继承
……我们需要一种更好的、全新的继承理论(目前仍是如此)。例如,继承和实例化(一种继承方式)混淆了语用(比如为了节省空间而重构代码)和语义(用途太多了,比如特殊化、普遍化、形态,等等)。
——Alan Kay
“The Early History of Smalltalk”
如 Alan Kay 所言,继承有很多用途,而多重继承增加了可选方案和复杂度。使用多重继承容易得出令人费解和脆弱的设计。我们还没有完整的理论,下面是避免把类图搅乱的一些建议。
多重继承,组合模糊,混入类