本章探讨继承和子类化,重点是说明对Python而言尤为重要的两个细节:
- 子类化内置类型的缺点
- 多重继承和方法解析顺序
12.1 子类化内置类型很麻烦
内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。
>>> 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
{'one': 1, 'two': [2, 2], 'three': 3}
原生类型的这种行为违背了面向对象变成的一个基本原则:始终应该从实例所属的类开始搜索方法。
不只实例内部的调用有这个问题,内置类型的方法调用的其他类的方法,如果被覆盖了,也不会被调用。
示例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'}
如果不实例化dict,而是子类化collections.UserDict。就不会有问题
示例12-3
>>> 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
{'one': [1, 1], 'two': [2, 2]}
>>> dd.update(three = 3)
>>> dd
{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}
>>> 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}
与继承,尤其是多重继承有关的另一个问题是:如果同级别的超类定义了同名属性,Python如果确定使用哪一个?
12.2 多重继承和方法解析顺序
任何实现多重继承的语言都要处理潜在的命令冲突,这种冲突由不相关的祖先类实现同名的方法引起。这种冲突称为“菱形问题”。
示例12-4 diamond.py:上图中的A、B、C、和D四个类
class A:
def ping(self):
print('ping:', self)
class B(A):
def pong(self):
print('pong:', self)
class C(A):
def pong(self):
print('PONG:', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self)
Python能却分d.pong()调用的是哪个方法,是因为Python会安装特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order, MRO)。类都有一个名为__mro__的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object类。
D.__mro__
(<class 'diamond.D'>, <class 'diamond.B'>,
<class 'python_cook.diamond.C'>,
<class 'diamond.A'>, <class 'object'>)
示例12-7 pingpong方法的5个调用
d = D()
d.pingpong()
ping: <diamond.D object at 0x7f8d7069acc0>
post-ping <diamond.D object at 0x7f8d7069acc0>
ping: <diamond.D object at 0x7f8d7069acc0>
pong: <diamond.D object at 0x7f8d7069acc0>
pong: <diamond.D object at 0x7f8d7069acc0>
PONG: <diamond.D object at 0x7f8d7069acc0>
示例12-8 查看几个类的__mro__属性
>>> bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)
>>> def print_mro(cls):
... print(','.join(c.__name__ for c in cls.__mro__))
...
>>> print_mro(bool)
bool,int,object
>>> import numbers
>>> print_mro(numbers.Integral)
Integral,Rational,Real,Complex,Number,object
>>> import io
>>> print_mro(io.BytesIO)
BytesIO,_BufferedIOBase,_IOBase,object
>>> print_mro(io.TextIOWrapper)
TextIOWrapper,_TextIOBase,_IOBase,object
Tkinter复杂的多重继承图
>>> import tkinter
>>> print_mro(tkinter.Text)
Text,Widget,BaseWidget,Misc,Pack,Place,Grid,XView,YView,object
>>>
12.3 多重继承的真实应用
Tkinter GUI类层次结构的UML简图
来看图中的几个类:
- Toplevel:表示Tkinter应用程序的顶层窗口的类
- Widget: 窗口中所有可见对象的超类
- Button: 普通的按钮小组件
- Entry: 单行可编辑文本字段
- Text:多行可编辑文本字段
>>> import tkinter
>>> print_mro(tkinter.Toplevel)
Toplevel,BaseWidget,Misc,Wm,object
>>> print_mro(tkinter.Widget)
Widget,BaseWidget,Misc,Pack,Place,Grid,object
>>> print_mro(tkinter.Button)
Button,Widget,BaseWidget,Misc,Pack,Place,Grid,object
>>> print_mro(tkinter.Entry)
Entry,Widget,BaseWidget,Misc,Pack,Place,Grid,XView,object
>>> print_mro(tkinter.Text)
Text,Widget,BaseWidget,Misc,Pack,Place,Grid,XView,YView,object
12.4 处理多重继承
下面是避免把类推搅乱的一些建议。
-
把接口继承和实现继承区分开
-
使用抽象基类显式表示接口
-
通过混入重用代码 如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class).
-
在名称中明确指明混入
-
抽象基类可以作为混入,反过来则不成立
-
不要子类化多个具体类
-
为用户提供聚合类 8.“优先使用对象组合,而不是类继承”
12.5 一个现代示例:Django通用视图中的混入
在Django中,视图是可调用的对象,它的参数是表示HTTP请求的对象,返回值是一个表示HTTP相应的对象。我们要关注的是这些响应对象。响应可以说简单的重定向,也可以说复杂的内容。