使用抽象基类时,多重继承很常见,而且实际上也是不可避免的,因为最基本的集合抽象基类(SequenceMappingSet)都扩展多个抽象基类。collections.abc 的源码(Lib/_collections_abc.py,https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py)是抽象基类使用多重继承的范例——其中很多还是混入类。

Raymond Hettinger 写的文章“Python's super() considered super!”(https://rhettinger.wordpress.com/2011/05/26/super-considered-super/),从积极的角度解说了 Python 的 super 和多重继承的运作原理。这篇文章是对 James Knight 的“Python's Super is nifty, but you can't use it”(以前题为“Python's Super Considered Harmful”,https://fuhm.net/super-harmful/)一文作出的回应。

尽管这两篇文章的题目中提到了内置的 super 函数,但它不是真正的问题——Python 3 中的 super 函数没有 Python 2 中那么令人讨厌了。真正的问题是多重继承,它天生复杂,难以处理。Michele Simionato 不再批评这一点,他在“Setting Multiple Inheritance Straight”一文(http://www.artima.com/weblogs/viewpost.jsp?thread=246488)中给出了解决方案:他实现了性状(trait),这是一种受限的混入,源自 Self 语言。Simionato 写了一系列具有启发性的博客文章,对 Python 的多重继承进行了探讨,包括“The wonders of cooperative inheritance, or using super in Python 3”(http://www.artima.com/weblogs/viewpost.jsp?thread=281127),“Mixins considered harmful”第一部分(http://www.artima.com/weblogs/viewpost.jsp?thread=246341)和第二部分(http://www.artima.com/weblogs/viewpost.jsp?thread=246483),以及“Things to Know About Python Super”第一部分(http://www.artima.com/weblogs/viewpost.jsp?thread=236275)、第二部分(http://www.artima.com/weblogs/viewpost.jsp?thread=236278)和第三部分(http://www.artima.com/weblogs/viewpost.jsp?thread=237121)。最早的文章使用 Python 2 的 super 句法,不过依然值得一读。

我读过 Grady Booch 写的《面向对象分析与设计(第 3 版)》,强烈推荐给你,这是面向对象思维的通用入门书,与具体的编程语言无关。很少有书能这样不带偏见地讨论多重继承。

杂谈

想想哪些类是真正需要的

大多数程序员编写应用程序而不开发框架。即便是开发框架的那些人,多数时候(或大多数时候)也是在编写应用程序。编写应用程序时,我们通常不用设计类的层次结构。我们至多会编写子类、继承抽象基类或框架提供的其他类。作为应用程序开发者,我们极少需要编写作为其他类的超类的类。我们自己编写的类几乎都是末端类(即继承树的叶子)。

如果作为应用程序开发者,你发现自己在构建多层类层次结构,可能是发生了下述事件中的一个或多个。

这些事情你可能都会遇到:你厌倦了,决定重新发明轮子,自己构建设计过度和不良的框架,因此不得不编写一个又一个类去解决鸡毛蒜皮的小事。希望你能乐在其中,至少得到应有的回报。

内置类型的不当行为是缺陷还是特性

内置的 dictliststr 类型是 Python 的底层基础,因此速度必须快,与这些内置类型有关的任何性能问题几乎都会对其他所有代码产生重大影响。于是,CPython 走了捷径,故意让内置类型的方法行为不当,即不调用被子类覆盖的方法。解决这一困境的可能方式之一是,为这些类型分别提供两种实现:一种供内部使用,为解释器做了优化;另一种供外部使用,便于扩展。

但是等等,我们已经拥有这些了:UserDictUserListUserString 虽然没有内置类型的速度快,但是易于扩展。CPython 采用的这种务实方式意味着,我们也要在自己的应用程序中使用做了优化但是难以子类化的实现。这是合理的,因为我们每天都使用 dictliststr,但是很少需要定制映射、列表或字符串。我们只需知道其中涉及的取舍。

其他语言对继承的支持

“面向对象”这个术语是 Alan Kay 发明的,而 Smalltalk 只支持单继承,不过有些派生版以不同的方式支持多重继承,例如现代的 Squeak 和 Smalltalk 方言 Pharo 支持性状(trait)——这是实现混入类的语言结构,而且能避免多重继承的一些问题。

C++ 是第一门实现多重继承的流行语言,但是这一功能被滥用了,因此意欲取代 C++ 的 Java 不支持多重继承(即没有混入类)。不过,Java 8 引入了默认方法,这使得接口与 C++ 和 Python 用于定义接口的抽象类十分相似。但是它们之间有个关键的区别:Java 的接口没有状态。Java 之后,使用最广泛的 JVM 语言要数 Scala 了,而它实现了性状。支持性状的其他语言还有最新稳定版 PHP 和 Groovy,以及正在开发的 Rust 和 Perl 6。因此可以说,性状是目前的趋势。

Ruby 对多重继承的态度很明确:对其不支持,但是引入了混入。Ruby 类的定义体中可以包含模块,这样模块中定义的方法就变成了类实现的一部分。这是“纯粹”的混入,不涉及继承,因此 Ruby 混入显然不会影响所在类的类型。这种方式凸显了混入的优点,避免了很多常见问题。

最近广受瞩目的两门语言——Go 和 Julia——对继承的支持极其有限。Go 完全不支持继承,但是它实现的接口与静态鸭子类型相似(详情参见第 11 章的“杂谈”)。Julia 回避“类”(class)这个术语,只接受“类型”(type)。Julia 有类型层次结构,但是子类型不能继承结构,只能继承行为,而且只能为抽象类型创建子类型。此外,Julia 的方法使用多重分派,这是 7.8.2 节所述机制的高级形式。