Python中的'super'是什么?- super().__init__()与显式父类__init__()的区别
这段内容在问:
class Child(SomeBaseClass):
def __init__(self):
super(Child, self).__init__()
和:
class Child(SomeBaseClass):
def __init__(self):
SomeBaseClass.__init__(self)
之间有什么区别?
我看到很多类里使用了 super
,这些类只有单一继承。我能理解在多重继承中使用它的原因,但在这种情况下使用它有什么好处我就不太清楚了。
这个问题主要是关于技术实现的细节,以及不同方式访问基类的 __init__
方法之间的区别。为了避免重复的问题,如果提问者只是忘记调用 super
,并且在问为什么基类的属性不可用,请参考这个链接:为什么我的子类实例不包含基类的属性(当我尝试使用它们时导致 AttributeError)?。
11 个回答
我之前玩过一点 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
如果想进一步了解,我推荐你看看以下几个回答:
有什么区别?
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
,它必须:
- 从实例的类型获取 MRO
- 查找定义该方法的类型
- 找到下一个有该方法的类型
- 绑定该方法并用预期的参数调用它
UnsuperChild
不应该访问InjectMe
。为什么结论不是“总是避免使用super
”?我错过了什么?
UnsuperChild
确实 不 能访问 InjectMe
。是 UnsuperInjector
能访问 InjectMe
- 但它无法从继承自 UnsuperChild
的方法中调用该类的方法。
两个子类都打算调用 MRO 中下一个同名的方法,这可能是它在创建时并不知道的 另一个 类。
没有 super
的那个类硬编码了它父类的方法 - 因此限制了它的方法行为,子类无法在调用链中注入功能。
而使用 super
的那个类则具有更大的灵活性。方法的调用链可以被拦截并注入功能。
你可能不需要这种功能,但你的代码的子类可能需要。
结论
总是使用 super
来引用父类,而不是硬编码它。
你想要引用的是下一个父类,而不是你看到的子类所继承的那个。
不使用 super
可能会给你的代码用户带来不必要的限制。
在单继承中,使用 super()
的好处其实不多,主要是你不需要在每个使用父类方法的方法里都写上父类的名字。
不过,如果你想用多重继承,那几乎离不开 super()
。这包括一些常见的用法,比如混入(mixins)、接口、抽象类等等。如果有人想在你的 Child
类基础上再加一个混入,他们的代码可能就会出问题。