《流畅的Python》笔记python
本篇是“面向对象惯用方法”的第五篇,咱们将继续讨论继承,重点说明两个方面:继承内置类型时的问题以及多重继承。概念比较多,较为枯燥。算法
内置类型(C语言编写)的方法一般会忽略用户重写的方法,这种行为体如今两方面:编程
A
的子类ChildA
即便重写了A
中的方法,当ChildA
调用这些方法时,也不必定调用的就是重写的版本,而依然可能调用A
中的版本;B
调用ChildA
的方法时,调用的也不必定是被ChildA
重写的方法,可能依然会调用A
的版本。以dict
的__getitem__
方法为例,即便这个方法被子类重写了,内置类型的get()
方法也不必定调用重写的版本:安全
# 代码1.1
>>> class MyDict(dict):
... def __getitem__(self, key):
... return "Test" # 无论要获取谁,都返回"Test"
...
>>> child = MyDict({"one":1, "two":2})
>>> child
{'one': 1, 'two': 2} # 正常
>>> child["one"]
'Test' # 此时也是正常的
>>> child.get("one")
1 # 这里就不正常了,按理说应该返回"Test"
>>> b = {}
>>> b.update(child)
>>> b # 并无调用child的__getitem__方法
{'one': 1, 'two': 2}
复制代码
这是在CPython中的状况,这些行为其实违背了面向对象编程的一个基本原则,即应该始终从实例所属的类开始搜索方法,即便在超类实现的类中调用也应该如此。但实际是可能直接调用基类的方法,而不先搜索子类。这种设定并不能说是错误的,这只是一种取舍,毕竟这也是CPython中的内置类型运行得快的缘由之一,但这种方式就给咱们出了难题。这种问题的解决方法有两个:微信
collections
模块中继承,好比继承自UserList
、UserDict
,UserString
。这些类不是用C语言写的,而是用纯Python写的,而且严格遵循了上述面向对象的原则。若是上述代码中的MyDict
继承自UserDict
,行为则会合乎预期。强调:本节所述问题只发生在C语言实现的内置类型内部的方法委托上,并且只影响直接继承内置类型的自定义类。若是子类继承自纯Python编写的类,则不会有此问题。框架
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的超类实现同名方法引发。这种冲突称为”菱形冲突“。函数
下面是咱们要实现的类的UML图:性能
红线表示超类的调用顺序,如下是它的实现:网站
# 代码2.1
class A:
def ping(self):
print("ping in A:", self)
class B(A):
def pong(self):
print("pong in B:", self)
class C(A):
def pong(self):
print("PONG in C:", self)
class D(B, C):
def ping(self):
super().ping()
print("ping in D:", self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self) # 在定义时调用特定父类的写法,显示传入self参数
# 下面是它在控制台中的调用状况
>>> from diamond import *
>>> d = D()
>>> d.pong()
pong in B: <mytest.D object at 0x0000013E66313048>
>>> d.pingpong()
ping in A: <mytest.D object at 0x0000013E66313048> # self.ping()
ping in D: <mytest.D object at 0x0000013E66313048>
ping in A: <mytest.D object at 0x0000013E66313048> # super().ping()
pong in B: <mytest.D object at 0x0000013E66313048> # self.pong()
pong in B: <mytest.D object at 0x0000013E66313048> # super().pong()
PONG in C: <mytest.D object at 0x0000013E66313048> # C.pong(self)
>>> C.pong(d) # 在运行时调用特定父类的写法,显示传入实例参数
PONG in C: <mytest.D object at 0x0000013E66313048>
>>> D.__mro__ # Method Resolutino Order,方法解析顺序,上一篇文章中有所说起
(<class 'mytest.D'>, <class 'mytest.B'>, <class 'mytest.C'>,
<class 'mytest.A'>, <class 'object'>)
复制代码
类都有一个名为__mro__
的属性,它的值是一个元组,按必定顺序列举超类,这个顺序由C3算法计算。spa
方法解析顺序不只考虑继承图,还考虑子类声明中列出超类的顺序。例如,若是D
类的声明改成class D(C, B)
,那么D
则会先搜索C
,再搜索B
。
若想把方法调用委托给超类,推荐的作法是使用内置的super()
函数;同时,还请注意上述调用特定超类的语法。然而,使用super()
是最安全的,也不易过期。调用框架或不受本身控制的类层次结构中的方法时,尤为应该使用super()
。
继承有不少用途,而多重继承增长了可选方案和复杂度。使用多重继承容易得出使人费解和脆弱的设计。如下是8条避免产生混乱类图的建议:
把接口继承和实现继承区分开
在使用多重继承时,必定要明白本身为何要建立子类:
其实这俩常常同时出现,不过只要有可能,必定要明确这么作的意图。经过继承重用代码是实现细节,一般能够换成用组合和委托的模式,而接口继承则是框架的支柱。
使用抽象基类显示表示接口
若是类的做用是定义接口,应该将其明肯定义为抽象基类。
经过“混入类”实现代码重用
若是一个类的做用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“is-a”关系,则应该把那个类明肯定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,并且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少许关系很是紧密的方法。
在名称中明确指明混入
因为Python没有把类明确声明为混入的正式方式,实际的作法是在类名后面加入Mixin
后缀。Python的GUI库Tkinter没有采用这种方法,这也是它的类图十分混乱的缘由之一,而Django则采用了这种方式。
抽象基类能够做为混入类,但混入类不能做为抽象基类
抽象基类能够实现具体方法,所以能够做为混入类使用。但抽象基类能定义数据类型,混入类则作不到。此外,抽象基类能够做为其余类的惟一基类,混入类则决不能做为惟一的基类,除非这个混入类继承了另外一个更具体的混入(这种作法很是少见)。
但值得注意的是,抽象基类中的具体方法只是一种便利措施,由于它只能调用抽象基类及其超类中定义了的方法,那么用户自行调用这些方法也能够实现一样的功能,因此,抽象基类也并不常做为混入类。
不要从多个具体类继承
应该尽可能保证具体类没有或者最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类外,其他的都应该是抽象基类或混入类。
为用户提供聚合类
若是抽象基类或混入类的组合对客户代码很是有用,那就提供一个类,使用易于理解的方式把它们结合起来,这种类被称为聚合类。好比tkinter.Widget
类,它的定义以下:
# 代码2.2
class Widget(BaseWidget, Pack, Place, Grid): # 省略掉了文档注释
pass
复制代码
它的定义体是空的,但经过这一个类,提供了四个超类的所有方法。
优先使用对象组合,而不是类继承
优先使用组合能让设计更灵活。即使是单继承,这个原则也能提高灵活性,由于继承是一种紧耦合,并且较高的继承树容易倒。组合和委托还能够代替混入类,把行为提供给不一样的类,但它不能取代接口继承,由于接口继承定义的是类层次结构。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~