本章将讨论继承和子类化,重点是说明对 Python 而言尤为重要的两个细节:
我们将通过两个重要的 Python 项目探讨多重继承,这两个项目是 GUI 工具包 Tkinter 和 Web 框架 Django
我们将首先分析子类化内置类型的问题,然后讨论多重继承,通过案例讨论类层次结构方面好的做法和不好的
在 Python 2.2 之前内置类型(如 list 和 dict)不能子类化,之后可以了,但是有个重要事项:内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法
至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有官方规定,基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖 __getitem__()
方法不会被内置类型的 get() 方法调用,下面说明了这个问题:
内置类型的 dict 的 __init__
和 __update__
方法会忽略我们覆盖的 __setitem__
方法
In [2]:
class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
dd = DoppelDict(one=1)
dd # 继承 dict 的 __init__ 方法忽略了我们覆盖的 __setitem__方法,'one' 值没有重复
Out[2]:
In [3]:
dd['two'] = 2 # `[]` 运算符会调用我们覆盖的 __setitem__ 方法
dd
Out[3]:
In [4]:
dd.update(three=3) #继承自 dict 的 update 方法也不会调用我们覆盖的 __setitem__ 方法
dd
Out[4]:
原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。在这种糟糕的局面中,__missing__
却能按照预期工作(3.4 节),但这是特例
不止实例内部有这个问题(self.get() 不调用 self.__getitem__()
),内置类型的方法调用其他类的方法,如果被覆盖了,也不会被调用。下面是个例子,改编自 PyPy 文档
dict.update 方法会忽略 AnswerDict.__getitem__
方法
In [5]:
class AnswerDict(dict):
def __getitem__(self, key):
return 42
ad = AnswerDict(a='foo')
ad['a'] # 返回 42,与预期相符
Out[5]:
In [6]:
d = {}
d.update(ad) # d 是 dict 的实例,使用 ad 中的值更新 d
d['a'] #dict.update 方法忽略了 AnswerDict.__getitem__ 方法
Out[6]:
直接子类化内置类型(如 dict,list,str)容易出错,因为内置类型的方法通常忽略用户覆盖的方法,不要子类化内置类型,用户自己定义的类应该继承 collections 模块中的类,例如 UserDict, UserList, UserString,这些类,这些类做了特殊设计,因此易于扩展
如果子类化的是 collections.UserDict,上面暴露的问题就迎刃而解了,如下:
In [8]:
import collections
class DoppelDict2(collections.UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
dd = DoppelDict2(one=1)
dd
Out[8]:
In [9]:
dd['two'] = 2
dd
Out[9]:
In [10]:
dd.update(three=3)
dd
Out[10]:
In [13]:
class AnswerDict2(collections.UserDict):
def __getitem__(self, key):
return 42
ad = AnswerDict2(a='foo')
ad['a']
Out[13]:
In [14]:
d = {}
d.update(ad)
d['a']
Out[14]:
In [15]:
d
Out[15]:
In [16]:
ad # 这里是自己加的,感觉还是有点问题,但是调用时候结果符合预期
Out[16]:
In [9]:
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)
B 和 C 都实现了 pong 方法,唯一区别就是打印不一样。在 D 上调用 d.pong 运行的是哪个 pong 方法呢? C++ 中,必须使用类名限定方法调用来避免歧义。Python 也可以,如下:
In [10]:
d = D()
d.pong() # 直接调用 d.pong() 是调用的 B 类中的版本
In [11]:
C.pong(d) #超类中的方法都可以直接调用,此时要把实例作为显式参数传入
Python 能区分 d.pong() 调用的是哪个方法,因为 Python 会按照特定的顺序遍历继承图,这个顺序叫顺序解析(Method Resolution Order,MRO)。类都有一个名为 __mro__
的属性,它的值是一个元组,按照方法解析顺序列出各个超类。从当前类一直向上,直到 object 类。D 类的 __mro__
属性如下:
In [12]:
D.__mro__
Out[12]:
若想把方法调用委托给超类,推荐的方法是使用内置的 super() 函数。在 Python 3 中,这种方式变得更容易了,如上面的 D 类中的 pingpong 方法所示。然而,有时可能幸亏绕过方法解析顺序,直接调用某个类的超方法 -- 这样有时更加方便。,例如,D.ping 方法可以这样写
In [13]:
def ping(self):
A.ping(self) # 而不是 super().ping()
print('post-ping', self)
注意,直接在类上调用实例方法时,必须显式传入 self 参数,因为这样访问的是未绑定方法(unbound method)
然而,使用 super() 最安全,也不易过时,调用框架或不受自己控制的类层次结构中的方法时,尤其适合用 super()。使用 super() 调用方法时,会遵循方法解析顺序,如下所示:
In [14]:
d = D()
d.ping() # 输出了两行,第一行是 super() A 类输出,第二行是 D 类输出
下面看看 D 在实例上调用 pingpong 方法得到的结果,如下所示:
In [17]:
d.pingpong() #最后一个是直接找到 C 类实现 pong 方法,忽略 mro
方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果声明 D 类时把 D 声明为 class D(C, B)
,那么 D 类的 __mro__
就会不一样,先搜索 C 类,再 搜索 B 类
分析类时,我们需要经常查看 __mro__
属性,下面是一些常用类的方法搜索顺序:
In [18]:
bool.__mro__
Out[18]:
In [19]:
def print_mro(cls):
print(', '.join(c.__name__ for c in cls.__mro__))
print_mro(bool)
In [21]:
import numbers
print_mro(numbers.Integral)
In [22]:
import io
print_mro(io.BytesIO)
In [23]:
print_mro(io.TextIOWrapper)
结束方法解析之前,我们再看看 Tkinter 复杂的多重继承:
In [24]:
import tkinter
print_mro(tkinter.Text)
研究 GUI 工具包中的 Tinker 时候,要从底部的 Text 类开始,这个类实现了多行可编辑文本小组文件,自身有丰富的功能,又从其他类继承了很多方法。
继承很强大,但也可能把类图搅乱,下面是一些建议:
把接口和实现继承区分开 使用多重继承时,一定要明确一开始为什么创建子类,主要原因可能有:
其实上面这两条经常同时出现,不过只要可能,一定要明确意图,通过继承重用代码是实现细节,通常可以换用组合和委托模式。而接口继承则是框架支柱
使用抽象基类显式的表示接口
现代 Python 中,如果类的作用是定义接口,应该明确把它定义为抽象基类。Python 3.4 以上的版本,我们要创建 abc.ABC 或其他抽象基类的子类(如果想支持较旧的 Python 版本,看第 11 章)
通过混入重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现"是什么“关系,应该把那个类明确的定义为混入类。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某些方面的特定行为,只实现少量关系非常紧密的方法
在名称中明确指明混入
由于 Python 没有把类声明为混入的正式方式,所以强烈推荐在名称中加入 ...Mixin 后缀
抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,因此也可以作为混入使用,不过抽象基类会定义类型,而混入类做不到,此外抽象基类可以作为其他类的唯一基类,而混入决不能作为唯一超类,除非继承另一个更具体的混入 -- 真实代码少有人这么做
不要子类化多个具体类
具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。例如,在下面代码中,如果 Alpha 是具体类,那么 Beta 和 Gamma 必须是抽象基类或混入
class MyConcreteClass(Alpha, Beta, Gamma):
'''这是一个具体类,可以实例化'''
为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式将他们结合起来。这种类称为聚合类
class Widget(BaseWidthget, Pack, Place, Grid):
'''Internal class'
Widget 类是空的,但是这个类提供了有用的服务:把四个超类结合在一起,这样需要创建新小组件的用户无需记住全部混入,也不用担心声明 class 语句时候有没有遵循特定的顺序
优先使用对象组合,而不是继承
优先使用组合能让设计更灵活,组合和委托可以代替混入,把行为提供给不同的类,但是不能取代接口继承去定义类型层次的结构
In [ ]: