Fluent Python 第十二章 继承的优缺点

Fluent Python 第十二章读书报告

Chapter 12. Inheritance: For Good or For Worse

第十二章: 继承的优缺点

推出继承的初衷是让新手顺利使用只有专家才能设计出来的框架。
—— Alan Kay

本章讨论继承和子类化,重点是说明对Python而言尤为重要的两个细节:

  • 子类化内置类型的缺点
  • 多重继承和方法解析顺序

12.1 子类化内置类型的麻烦之处

Python中的内置类型子类化有一个重要的注意事项:内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。

内置类型的这种行为违背了面向对象编程的一个基本原则:从实例所属的类搜索方法,这是一个十分糟糕的局面。

所以在子类化的时候,不应该选择内置类型,而应该选择collections中的类进行继承。

12.2 多重继承和方法解析顺序

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突是由不相关的祖先类同名的方法引起的,称为“菱形问题”。

Figure-12-1

python能区分子类调用的是哪一个方法,是因为Python会按照特定的顺序遍历继承图,这个顺序叫做方法解析顺序(Method Resolution Order,MRO), Python中的类都有一个名为__mro__的属性,它的值是一个元组,按照方法戒子顺序列出各个超类,从当前类一直向上,直到object类。例如tenserflow中的Variable类

1
2
3
>>> import tensorflow as tf
>>> tf.Variable.__mro__
>>> (<class 'tensorflow.python.ops.variables.Variable'>, <class 'tensorflow.python.training.checkpointable.base.CheckpointableBase'>, <class 'object'>)

如果把方法调用委托给超类,可以有下面两种选择:

  • 使用super()函数如Foo_son.super().bar(), 这个方法的好处在于安全,不会过时。适合使用的场景: 调用框架或者不受自己控制的类层次结构中的方法
  • 如果想要绕过方法解析, 直接调用超类的某个方法,可以直接使用超类类名代替super()函数,如Foo.bar()

方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果把 D 类声明为 class D(C, B):,那么 D 类的__mro__ 属性就是:先搜索 C 类,再搜索 B 类。

12.3 多重继承的真实应用

标准库中,GUI工具包Tkinter把多重继承用到了极致。
Figure-12-2

其中几个类:

  • Toplevel:表示 Tkinter 应用程序中顶层窗口的类。
  • Widget:窗口中所有可见对象的超类。
  • Button:普通的按钮小组件。
  • Entry:单行可编辑文本字段。
  • Text:多行可编辑文本字段

但由于Tkinter开发时间比较久远,很多使用多重继承不能称作是最佳的实践。后面会对Tkinter使用多重继承的可取之处不当之处进行讨论

12.4 处理多重继承

……我们需要一种更好的、全新的继承理论(目前仍是如此)。例如,继承和实例化(一种继承方式)混淆了语用(比如为了节省空间而重构代码)和语义(用途太多了,比如特殊化、普遍化、形态,等等)。
——Alan Kay

如上面引用的一段话, 继承有很多用途,而多重继承增加了可选方案和复杂度。使用多重继承容易得出令人费解和脆弱的设计。

暂时还没有针对多重继承完整的理论,下面是避免把类图搅乱的一些建议。

  • 把接口继承和实现继承区分开来
    使用多重继承是,一定要明确为什么创建子类。一般原因可能有:

    • 继承接口,创建子类型, 实现“是什么”
    • 继承实现,通过继承避免代码复用
      其实这两条经常同时出现,不过只要可能的话,一定要明确区分。前者是支撑框架的支柱,后者是实现细节,通常可以换用组合和委托模式。
  • 使用抽象基类显式表示接口
    如果类的作用是定义接口,应该明确定为抽象基类

  • 通过混入(mixin)重用代码
    如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。

  • 在名称中明确指明混入
    混入类应该以Mixin 作为后缀(在django的GenericViews中尤为常见)

  • 抽象基类可以作为混入,反之不成立。
    抽象基类可以实现具体方法,因此也可以作为混入使用。抽象基类可以作为其他类的唯一基类,而混入决不能作为唯一的超类,除非继承另一个更具体的混入。
    抽象基类中实现的具体方法只能与抽象基类及其超类中的方法协作。

  • 不要子类化多个具体类
    具体类的超类中除了一个具体超类之外,其余的都应该是抽象基类或混入。

  • 为用户提供聚合类
    如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们结合起来。 Grady Booch 把这种类称为聚合类(aggregate class)。
    聚合类恰如其名,只做聚合,不额外实现类方法

  • “优先使用对象组合,而不是类继承”
    优先使用组合能让设计更灵活。组合和委托可以代替混入,把行为提供给不同的类,但是不能取代接口继承去定义类型层次结构。

12.5 小结

本章着重探讨了多重继承这把双刃剑。讲述了Python中的MRO(方法解析顺序),探讨了多重继承实现的一些可以称之为准则的最佳实践标准。