>>> from env_helper import info; info()
页面更新时间: 2024-03-29 16:20:49
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.12. 继承的优缺点

(我们)推出继承的初衷是让新手顺利使用只有专家才能设计出来的框架。
——Alan Klay, “The Early History of Smalltalk”

本章探讨继承和子类化,重点是说明对 Python 而言尤为重要的两个细节: * 子类化内置类型的缺点 * 多重继承和方法解析顺序(MRO)

1.12.1. 子类化类型的缺点

基本上,内置类型的方法不会调用子类覆盖的方法,所以尽可能不要去子类化内置类型。
如果有需要使用 list, dict 等类,collections 模块中提供了用于用户继承的 UserDictuserListUserString,这些类经过特殊设计,因此易于扩展。
>>> # 子类化内置类型的缺点
>>> class DoppelDict(dict):
>>>     def __setitem__(self, key, value):
>>>         super().__setitem__(key, [value] * 2)
>>>
>>> # 构造方法和 update 都不会调用子类的 __setitem__
>>> dd = DoppelDict(one=1)
>>> dd['two'] = 2
>>> print(dd)
>>> dd.update(three=3)
>>> print(dd)
>>>
>>>
>>> from collections import UserDict
>>> class DoppelDict2(UserDict):
>>>     def __setitem__(self, key, value):
>>>         super().__setitem__(key, [value] * 2)
>>>
>>> # UserDict 中,__setitem__ 对 update 起了作用,但构造函数依然不会调用 __setitem__
>>> dd = DoppelDict2(one=1)
>>> dd['two'] = 2
>>> print(dd)
>>> dd.update(three=3)
>>> print(dd)
{'one': 1, 'two': [2, 2]}
{'one': 1, 'two': [2, 2], 'three': 3}
{'one': [1, 1], 'two': [2, 2]}
{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}

1.12.2. 方法解析顺序(Method Resolution Order)

> Moreover, unless you make strong use of multiple inheritance and you have non-trivial hierarchies, you don’t need to understand the C3 algorithm, and you can easily skip this paper.

Emmm…

OK,提两句: 1. 如果想查看某个类的方法解析顺序,可以访问该类的 __mro__ 属性; 2. 如果想绕过 MRO 访问某个父类的方法,可以直接调用父类上的非绑定方法。

>>> class A:
>>>     def f(self):
>>>         print('A')
>>>
>>>
>>> class B(A):
>>>     def f(self):
>>>         print('B')
>>>
>>>
>>> class C(A):
>>>     def f(self):
>>>         print('C')
>>>
>>>
>>> class D(B, C):
>>>     def f(self):
>>>         print('D')
>>>
>>>     def b_f(self):
>>>         "D -> B"
>>>         super().f()
>>>
>>>     def c_f(self):
>>>         "B -> C"
>>>         super(B, self).f()
>>>         # C.f(self)
>>>
>>>     def a_f(self):
>>>         "C -> A"
>>>         super(C, self).f()
>>>
>>>
>>> print(D.__mro__)
>>> d = D()
>>> d.f()
>>> d.b_f()
>>> d.c_f()
>>> d.a_f()
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
D
B
C
A

1.12.3. 处理多重继承

书中列出来了一些处理多重继承的建议,以免做出令人费解和脆弱的继承设计: 1. 把接口继承和实现继承区分开
如果继承重用的是代码实现细节,通常可以换用组合和委托模式。 2. 使用抽象基类显式表示接口
如果基类的作用是定义接口,那就应该定义抽象基类。 3. 通过混入(Mixin)重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现实际的“上下级”关系,那就应该明确地将这个类定义为混入类(Mixin class)。关于 Mixin(我还是习惯英文名),可以看 Python3-Cookbook 的《利用Mixins扩展类功能》章节。 4. 在名称中明确指出混入
在类名里使用 XXMixin 写明这个类是一个 Mixin. 5. 抽象基类可以作为混入,反过来则不成立 6. 不要子类化多个具体类
在设计子类时,不要在多个具体基类上实现多继承。一个子类最好只继承自一个具体基类,其余基类最好应为 Mixin,用于提供增强功能。 7. 为用户提供聚合类
如果抽象基类或 Mixin 的组合对客户代码非常有用,那就替客户实现一个包含多继承的聚合类,这样用户可以直接继承自你的聚合类,而不需要再引入 Mixin. 8. “优先使用对象组合,而不是类继承”
组合和委托可以代替混入,把行为提供给不同的类,不过这些方法不能取代接口继承去定义类型层次结构
两个实际例子:
* 很老的 tkinter 称为了反例,那个时候的人们还没有充分认识到多重继承的缺点; * 现代的 Django 很好地利用了继承和 Mixin。它提供了非常多的 View 类,鼓励用户去使用这些类以免除大量模板代码。