super 与多重继承
在介绍多重继承与 super 之前……先提醒一下:这部分可能会变得有点奇葩且复杂。
首先,什么是多重继承?到目前为止,示例代码都只涉及单个子类继承自单个父类。在多重继承中,会有多个父类。一个子类可以从 2、3、10 等多个父类继承。
这正是 super 优势更加明显的地方。除了省去引用不同父类名的击键数之外,在多重继承模式中使用 super 还有一些微妙的好处。简而言之,如果您要使用多重继承,请使用 super。
不使用 super 的多重继承
我们先看一个多重继承的例子,它避免修改任何父类方法,因此也就不需要 super。
输入
class B:
def b(self):
print('b')
class C:
def c(self):
print('c')
class D(B, C):
def d(self):
print('d')
d = D()
d.b()
d.c()
d.d()
输出
b
c
d
多方法解析顺序
考虑到多重继承的概念,这个输出并不令人意外。D 从父类继承了方法 x 和 z,一切看起来……暂时还算顺利。
那么,如果 B 和 C 都有同名方法会怎样?这就引出了“多方法解析顺序”(method resolution order),简称 MRO。子类的 MRO 决定了 Python 在哪里查找某个方法,以及当发生冲突时会调用哪个方法。
我们来看一个例子。
输入
class B:
def x(self):
print('x: B')
class C:
def x(self):
print('x: C')
class D(B, C):
pass
d = D()
d.x()
print(D.mro())
输出
x: B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
当我们调用继承而来的 x 方法时,只看到了从 B 继承的输出。我们可以通过调用 mro 类方法来查看 D 类的 MRO。从 D.mro() 的输出可以得知:程序会默认先尝试调用 D 自身的方法,然后是 B,再到 C,最后是 object。如果这些地方都找不到,就会抛出 D 没有所请求方法的错误。
值得注意的是,默认情况下每个类都继承自 object,它位于每个 MRO 的末尾。
多重继承、super 与菱形问题
下面是一个使用 super 以有利方式处理 __init__ 的 MRO 的例子。在该示例中,我们创建一组文本处理类,并在另一个采用多重继承的类中组合它们的功能。我们将创建 4 个类,其继承结构如下图所示。
注意:该结构仅用于说明目的;在不受限制的情况下,实现该逻辑有更好的方式。

这实际上是多重继承中“菱形问题”的一个例子。其名称当然源于设计结构的形状,以及它确实相当令人困惑。
下面用 super 实现上述设计。
输入
class Tokenizer:
"""Tokenize text"""
def __init__(self, text):
print('Start Tokenizer.__init__()')
self.tokens = text.split()
print('End Tokenizer.__init__()')
class WordCounter(Tokenizer):
"""Count words in text"""
def __init__(self, text):
print('Start WordCounter.__init__()')
super().__init__(text)
self.word_count = len(self.tokens)
print('End WordCounter.__init__()')
class Vocabulary(Tokenizer):
"""Find unique words in text"""
def __init__(self, text):
print('Start init Vocabulary.__init__()')
super().__init__(text)
self.vocab = set(self.tokens)
print('End init Vocabulary.__init__()')
class TextDescriber(WordCounter, Vocabulary):
"""Describe text with multiple metrics"""
def __init__(self, text):
print('Start init TextDescriber.__init__()')
super().__init__(text)
print('End init TextDescriber.__init__()')
td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)
输出
Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5
首先,我们看到 TextDescriber 类继承了这个类族树中的所有属性。得益于多重继承,我们可以“组合”多个类的功能。
接下来讨论来自各个类 init 方法的打印输出:
每个 __init__ 方法都被调用且仅调用一次。
TextDescriber 类继承自两个又继承自 Tokenizer 的类。为什么 Tokenizer.__init__ 没有被调用两次?
如果我们把 所有 对 super 的调用都换回老方式,我们最终会调用两次 Tokenizer.__init__。而使用 super 的调用会更“聪明”地考虑继承关系,并跳过对 A 的多余调用。
在其他方法结束之前,每个 __init__ 方法都已经开始执行。
各个 __init__ 的开始与结束顺序值得注意,尤其当您试图设置与另一父类发生命名冲突的属性时。该属性会被覆盖,这会变得非常混乱。
在我们的例子中,我们避免了继承属性的命名冲突,因此一切如预期运作。
重申一下,菱形问题会很快变得复杂,并导致出乎意料的结果。和大多数编程情形一样,最好避免过于复杂的设计。
我们学到了什么
- 我们了解了
super函数,以及它如何在单继承中替代ParentName.method的用法。这通常更易于维护。 - 我们了解了多重继承,以及如何将多个父类的功能传递给一个子类。
- 我们了解了方法解析顺序(MRO),以及在父类方法发生同名冲突时,它如何决定多重继承中的行为。
- 我们了解了菱形问题,并通过一个示例看到使用
super如何穿越“菱形”。
继承快速概览
随着您的 Python 项目和包不断增长,您不可避免地会想要使用类,并在此过程中应用 DRY(不要重复自己)原则。类继承是一个极好的方式:基于另一个类来创建类,从而保持 DRY。本文将介绍更高级的继承概念,基础继承不会深入展开。我们会做一个简短的引入,但网上有更好、更详细的入门教程。以下是一些入门资源:Object-Oriented Programming in Python 课程和 Python Object-Oriented Programming (OOP): Tutorial。
那么,什么是类继承?类似遗传学,子类可以从父类“继承”属性和方法。让我们直接通过代码示例来看看。下面的代码块中,我们用一个从
Parent类继承的Child类来演示继承。输入
输出
我们可以看到,
Child类从Parent类“继承”了属性和方法。无需额外工作,Parent.parent_method就成为了Child类的一部分。为了获得Parent.__init__()方法的好处,我们需要显式调用该方法并传入self。这是因为当我们在Child中添加了一个__init__方法时,便覆盖了继承而来的__init__。简短但并不全面的概览到此为止,接下来进入本文的核心内容。
super简介在最简单的情况下,
super函数可以用来替代对Parent.__init__(self)的显式调用。我们可以用super重写第一部分中的入门示例,如下所示。请注意,下面的代码块以 Python 3 语法编写,更早的版本语法略有不同。另外,由于输出与第一个代码块完全相同,这里省略了输出。老实说,在这种情况下使用
super带来的优势很小,甚至可能没有。根据父类名称的长度,我们也许能省点击键数,而且调用__init__时不需要传入self。下面是单继承场景中使用super的一些利弊。缺点
有人认为,这里使用
super会让代码变得不够显式。而让代码变得不够显式,违背了 The Zen of Python(Python 之禅)中的“显式优于隐式”。优点
即便在单继承中,使用
super也有可维护性的好处。若因某种原因子类的继承关系发生变化(例如父类更换,或者转为多重继承),那么无需到处查找并替换所有遗留的ParentClass.method_name()引用;使用super能让这些变化随着class声明的修改而自然生效。