今天讨论两个话题
* 子类化内置类型的缺点
* 多重继承和方法解析顺序(__mro__)
许多人都对继承敬而远之。Java不支持多继承,并没有产生什么坏的影响,而C++对多继承的滥用上了很多人的心(笔者也是其中一位)。因此,今天就讨论一下多继承到底是怎么回事。
子类化内置类型很麻烦
直接子类化内置类型(如继承list、dict、str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。因此不要子类化内置类型,用户应该继承collections模块中的类,UserDict、UserList、UserString等,这些类是python提供给用户用来扩展的。
多重继承和方法解析顺序
与继承尤其是多继承密切相关的另一个问题是:如果同级的父类有个同名方法或属性,那么python如何决定使用哪一个?
作为一个曾经的C++程序员,经常要面临这个问题。实际上,任何支持多继承的语言都要面临这种潜在的命名冲突,这种冲突由不相关的父类实现了同名的方法引起,这就是经典的”菱形问题“。
举例说明如下:
1 class A:
2 def ping(self): 3 print("ping:", self) 4 5 class B(A): 6 def pong(self): 7 print("pong:", self) 8 9 class C(A): 10 def pong(self): 11 print("PONG:", self) 12 13 class D(B, C): 14 def ping(self): 15 super(D, self).ping() 16 print('post-ping:', self) 17 18 def pingpong(self): 19 self.ping() 20 print(1) 21 super(D, self).ping() 22 print(2) 23 self.pong() 24 print(3) 25 super(D, self).pong() 26 print(4) 27 C.pong(self)
类B、C继承类A,且都实现了pong方法,但是打印的内容不一样。
如果D的实例调用pong方法的话,调用的是C的还是B的呢?答案是B的pong方法。
类有一个名为__mro__的属性,它是个元组,python会按照__mro__的值按照方法解析出各个父类,知道object类为止。如果想调用父类的方法,推荐使用super()函数。你也可以使用类名.方法(self)的方式调用父类的方法,但是不推荐,如果想绕过方法解析顺序可以使用。
类的继承关系和__mro__解析顺序如下图:
1 d = D()
2 d.ping() 3 print("-------------------------------") 4 d.pingpong() 5 6 7 """ 8 运行结果 9 ping: <__main__.D object at 0x00000000035F62E8> 10 post-ping: <__main__.D object at 0x00000000035F62E8> 11 ------------------------------- 12 ping: <__main__.D object at 0x00000000035F62E8> 13 post-ping: <__main__.D object at 0x00000000035F62E8> 14 1 15 ping: <__main__.D object at 0x00000000035F62E8> 16 2 17 pong: <__main__.D object at 0x00000000035F62E8> 18 3 19 pong: <__main__.D object at 0x00000000035F62E8> 20 4 21 PONG: <__main__.D object at 0x00000000035F62E8> 22 """
方法解析顺序不仅跟继承关系有关,还跟子类中声明的父类顺序有关。如果把类的声明顺序改变,那方法解析顺序也会改变:
1 class E(B, C):
2 pass
3
4 class F(C, B): 5 pass 6 7 print(E.__mro__) 8 print(F.__mro__) 9 10 """ 11 (<class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) 12 (<class '__main__.F'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>) 13 """
方法解析顺序依赖于C3算法。详见https://www.python.org/download/releases/2.3/mro/
GUI工具包Tkinter的继承关系图如下:
加入Text的声明顺序是:
class Text(YView, XView, Widget):
...
那么方法解析顺序就应该是:
Text -> YView -> XView -> Widget -> Grid -> Place -> Pack -> BaseWidget -> Misc -> object
使用多重继承的一些建议
《设计模式:可复用面向对象软件的基础》中的适配器模式用的就是多重继承(但是其他22个设计模式都是用单继承,可见多重继承显然是不推荐使用)。
使用多重继承容易得到不易理解和脆弱的系统设计,书中给出了一些关于继承的建议:
1 把接口继承跟实现继承区分开
使用多重继承时,一定要明确为什么创建子类,原因大概有二:
* 继承接口,创建子类型,实现"是什么"的关系
* 继承实现,避免代码重复
避免代码重复通常可以替换成组合和委托模式,二接口继承则是框架的支柱。
2、使用抽象基类表示接口
如果类的作用是定义接口,应该把它明确声明为抽象基类
3、通过混入重用代码
如果类的作用是为不同的子类提供方法,从而实现重用,但子类不是"是什么"的关系,应该把这个类定义为混入类(mixin class)。混入类通常以xxxMixin命名,而且不能实例化,子类不能只继承混入类。混入类应提供某方面的特定行为,只实现少了关系非常密切的方法。
4、在名称中明确指明混入
5、抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,所以可以作为混入类。然而,抽象基类可以派生子类,但是混入类不行。
6、不要子类化多个具体类
具体类做多这有一个具体超类。在具体类的继承超类中,最多只有一个具体超类,其他则是抽象基类或者混入。假设有如下代码:
1 class MyConcreteClass(Alpha, Beta, Gamma):
2 """
3 不要子类化多个具体类
4 """
如果Alpha是具体类, 那么Beta和Gamma都应该是抽象基类或混入。
7、为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,用易于理解的方式把他们结合起来。
8、优先使用对象组合而不是类继承