跳至主要内容

多重继承与 super() 入门

面向 Python 开发者的多重继承与 super() 入门指南,以及如何处理菱形继承问题。
· 8分钟 阅读

使用 AI 探索

继承快速概览

随着您的 Python 项目和包不断增长,您不可避免地会想要使用类,并在此过程中应用 DRY(不要重复自己)原则。类继承是一个极好的方式:基于另一个类来创建类,从而保持 DRY。本文将介绍更高级的继承概念,基础继承不会深入展开。我们会做一个简短的引入,但网上有更好、更详细的入门教程。以下是一些入门资源:Object-Oriented Programming in Python 课程和 Python Object-Oriented Programming (OOP): Tutorial。

那么,什么是类继承?类似遗传学,子类可以从父类“继承”属性和方法。让我们直接通过代码示例来看看。下面的代码块中,我们用一个从 Parent 类继承的 Child 类来演示继承。

输入

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)
        self.child_attribute = 'I am a child'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

输出

I am a child
I am a parent
Back in my day...

我们可以看到,Child 类从 Parent 类“继承”了属性和方法。无需额外工作,Parent.parent_method 就成为了 Child 类的一部分。为了获得 Parent.__init__() 方法的好处,我们需要显式调用该方法并传入 self。这是因为当我们在 Child 中添加了一个 __init__ 方法时,便覆盖了继承而来的 __init__。

简短但并不全面的概览到此为止,接下来进入本文的核心内容。

super 简介

在最简单的情况下,super 函数可以用来替代对 Parent.__init__(self) 的显式调用。我们可以用 super 重写第一部分中的入门示例,如下所示。请注意,下面的代码块以 Python 3 语法编写,更早的版本语法略有不同。另外,由于输出与第一个代码块完全相同,这里省略了输出。

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_attribute = 'I am a parent'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

老实说,在这种情况下使用 super 带来的优势很小,甚至可能没有。根据父类名称的长度,我们也许能省点击键数,而且调用 __init__ 时不需要传入 self。下面是单继承场景中使用 super 的一些利弊。

缺点

有人认为,这里使用 super 会让代码变得不够显式。而让代码变得不够显式,违背了 The Zen of Python(Python 之禅)中的“显式优于隐式”。

优点

即便在单继承中,使用 super 也有可维护性的好处。若因某种原因子类的继承关系发生变化(例如父类更换,或者转为多重继承),那么无需到处查找并替换所有遗留的 ParentClass.method_name() 引用;使用 super 能让这些变化随着 class 声明的修改而自然生效。

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 个类,其继承结构如下图所示。

注意:该结构仅用于说明目的;在不受限制的情况下,实现该逻辑有更好的方式。

diamond problem

这实际上是多重继承中“菱形问题”的一个例子。其名称当然源于设计结构的形状,以及它确实相当令人困惑。

下面用 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 如何穿越“菱形”。

主题