继承的优缺点

今天讨论两个话题

* 子类化内置类型的缺点

* 多重继承和方法解析顺序(__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、优先使用对象组合而不是类继承

  

posted on 2019-03-06 18:12  forwardFields  阅读(2104)  评论(0编辑  收藏  举报

导航