Python中的'super'是什么?- super().__init__()与显式父类__init__()的区别

749 投票
11 回答
298752 浏览
提问于 2025-04-11 09:28

这段内容在问:

class Child(SomeBaseClass):
    def __init__(self):
        super(Child, self).__init__()

和:

class Child(SomeBaseClass):
    def __init__(self):
        SomeBaseClass.__init__(self)

之间有什么区别?

我看到很多类里使用了 super,这些类只有单一继承。我能理解在多重继承中使用它的原因,但在这种情况下使用它有什么好处我就不太清楚了。


这个问题主要是关于技术实现的细节,以及不同方式访问基类的 __init__ 方法之间的区别。为了避免重复的问题,如果提问者只是忘记调用 super,并且在问为什么基类的属性不可用,请参考这个链接:为什么我的子类实例不包含基类的属性(当我尝试使用它们时导致 AttributeError)?

11 个回答

61

我之前玩过一点 super(),发现我们可以改变调用的顺序。

比如,我们有下面这个类的层级结构:

    A
   / \
  B   C
   \ /
    D

在这种情况下,D 的 MRO(方法解析顺序)是这样的(仅适用于 Python 3):

In [26]: D.__mro__
Out[26]: (__main__.D, __main__.B, __main__.C, __main__.A, object)

接下来,我们创建一个类,在方法执行后调用 super()

In [23]: class A(object): #  or with Python 3 can define class A:
...:     def __init__(self):
...:         print("I'm from A")
...:  
...: class B(A):
...:      def __init__(self):
...:          print("I'm from B")
...:          super().__init__()
...:   
...: class C(A):
...:      def __init__(self):
...:          print("I'm from C")
...:          super().__init__()
...:  
...: class D(B, C):
...:      def __init__(self):
...:          print("I'm from D")
...:          super().__init__()
...: d = D()
...:
I'm from D
I'm from B
I'm from C
I'm from A

    A
   / ⇖
  B ⇒ C
   ⇖ /
    D

这样我们就能看到解析顺序和 MRO 是一样的。但是当我们在方法开始时调用 super() 时:

In [21]: class A(object):  # or class A:
...:     def __init__(self):
...:         print("I'm from A")
...:  
...: class B(A):
...:      def __init__(self):
...:          super().__init__()  # or super(B, self).__init_()
...:          print("I'm from B")
...:   
...: class C(A):
...:      def __init__(self):
...:          super().__init__()
...:          print("I'm from C")
...:  
...: class D(B, C):
...:      def __init__(self):
...:          super().__init__()
...:          print("I'm from D")
...: d = D()
...: 
I'm from A
I'm from C
I'm from B
I'm from D

我们得到的顺序就变了,和 MRO 的顺序是相反的。

    A
   / ⇘
  B ⇐ C
   ⇘ /
    D 

如果想进一步了解,我推荐你看看以下几个回答:

  1. C3 线性化示例与 super(一个大的层级)
  2. 旧式和新式类之间的重要行为变化
  3. 新式类的内幕故事
469

有什么区别?

SomeBaseClass.__init__(self) 

这意味着调用 SomeBaseClass__init__ 方法,而

super().__init__()

这意味着调用一个绑定的 __init__ 方法,这个方法来自于在实例的方法解析顺序(MRO)中紧跟在 SomeBaseClass 子类之后的父类。

如果这个实例是这个子类的子类,MRO 中可能会有不同的父类。

简单解释

当你写一个类时,你希望其他类能够使用它。super() 让其他类更容易使用你正在编写的类。

正如 Bob Martin 所说,一个好的架构允许你尽可能推迟决策。

super() 可以实现这种架构。

当另一个类继承你写的类时,它也可能继承其他类。而这些类可能有一个 __init__ 方法,这个方法在这个 __init__ 方法之后,具体取决于类的顺序。

如果没有 super,你可能会硬编码你正在编写的类的父类(就像例子中那样)。这意味着你不会调用 MRO 中下一个 __init__ 方法,因此无法重用其中的代码。

如果你是为个人使用编写代码,可能不太在意这个区别。但如果你希望其他人使用你的代码,使用 super 可以为代码的用户提供更大的灵活性。

Python 2 和 3 的区别

这在 Python 2 和 3 中都能工作:

super(Child, self).__init__()

这仅在 Python 3 中有效:

super().__init__()

它通过向上移动到调用栈的上层,获取方法的第一个参数(通常是实例方法的 self 或类方法的 cls - 但也可能是其他名字),并在自由变量中找到类(例如 Child),这个过程是通过 __class__ 作为自由闭包变量在方法中查找的。

我以前喜欢演示跨版本兼容的 super 使用方式,但现在 Python 2 已经基本被淘汰,我将演示 Python 3 的用法,也就是不带参数地调用 super

间接性与向前兼容

这给你带来了什么?对于单继承,问题中的例子在静态分析的角度上几乎是相同的。然而,使用 super 给你提供了一层间接性和向前兼容性。

向前兼容性对经验丰富的开发者来说非常重要。你希望你的代码在修改时尽量少变动地继续工作。当你查看修订历史时,你希望清楚地看到每次修改的具体内容。

你可能一开始使用单继承,但如果你决定添加另一个基类,你只需要更改基类的那一行 - 如果你继承的类中的基类发生变化(比如添加了一个混入类),你在这个类中就不需要做任何更改。

在 Python 2 中,正确获取 super 的参数和方法参数可能有点混乱,所以我建议使用仅适用于 Python 3 的调用方式。

如果你知道在单继承中正确使用 super,那么在调试时会更轻松。

依赖注入

其他人可以使用你的代码,并在方法解析中注入父类:

class SomeBaseClass(object):
    def __init__(self):
        print('SomeBaseClass.__init__(self) called')
    
class UnsuperChild(SomeBaseClass):
    def __init__(self):
        print('UnsuperChild.__init__(self) called')
        SomeBaseClass.__init__(self)
    
class SuperChild(SomeBaseClass):
    def __init__(self):
        print('SuperChild.__init__(self) called')
        super().__init__()

假设你在对象中添加了另一个类,并希望在 Foo 和 Bar 之间注入一个类(出于测试或其他原因):

class InjectMe(SomeBaseClass):
    def __init__(self):
        print('InjectMe.__init__(self) called')
        super().__init__()

class UnsuperInjector(UnsuperChild, InjectMe): pass

class SuperInjector(SuperChild, InjectMe): pass

使用不带 super 的子类无法注入依赖,因为你使用的子类硬编码了在其自身之后调用的方法:

>>> o = UnsuperInjector()
UnsuperChild.__init__(self) called
SomeBaseClass.__init__(self) called

然而,使用 super 的子类可以正确注入依赖:

>>> o2 = SuperInjector()
SuperChild.__init__(self) called
InjectMe.__init__(self) called
SomeBaseClass.__init__(self) called

回应评论

这有什么用呢?

Python 通过 C3 线性化算法 将复杂的继承树线性化,以创建方法解析顺序(MRO)。

我们希望方法按照这个顺序被查找。

为了让父类中定义的方法找到下一个方法,如果没有 super,它必须:

  1. 从实例的类型获取 MRO
  2. 查找定义该方法的类型
  3. 找到下一个有该方法的类型
  4. 绑定该方法并用预期的参数调用它

UnsuperChild 不应该访问 InjectMe。为什么结论不是“总是避免使用 super”?我错过了什么?

UnsuperChild 确实 能访问 InjectMe。是 UnsuperInjector 能访问 InjectMe - 但它无法从继承自 UnsuperChild 的方法中调用该类的方法。

两个子类都打算调用 MRO 中下一个同名的方法,这可能是它在创建时并不知道的 另一个 类。

没有 super 的那个类硬编码了它父类的方法 - 因此限制了它的方法行为,子类无法在调用链中注入功能。

而使用 super 的那个类则具有更大的灵活性。方法的调用链可以被拦截并注入功能。

你可能不需要这种功能,但你的代码的子类可能需要。

结论

总是使用 super 来引用父类,而不是硬编码它。

你想要引用的是下一个父类,而不是你看到的子类所继承的那个。

不使用 super 可能会给你的代码用户带来不必要的限制。

356

在单继承中,使用 super() 的好处其实不多,主要是你不需要在每个使用父类方法的方法里都写上父类的名字。

不过,如果你想用多重继承,那几乎离不开 super()。这包括一些常见的用法,比如混入(mixins)、接口、抽象类等等。如果有人想在你的 Child 类基础上再加一个混入,他们的代码可能就会出问题。

撰写回答