1. 构造函数(__init__(self[, ...]))
在类中定义 __init__() 方法,可以实现在实例化对象的时候进行个性化定制:
>>> class C:
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def add(self):
... return self.x + self.y
... def mul(self):
... return self.x * self.y
...
>>> c = C(2, 3)
>>> c.add()
5
>>> c.mul()
6
2. 重写
前面我们在 “继承” 中讲过,如果对于父类的某个属性或方法不满意的话,完全可以重新写一个同名的属性或方法对其进行覆盖。那么这种行为,我们就称之为是子类对父类的重写。
这里,我们可以定义一个新的类 —— D,继承自上面的类 C,然后对 add() 和 mul() 方法进行重写:
>>> class D(C):
... def __init__(self, x, y, z):
... C.__init__(self, x, y)
... self.z = z
... def add(self):
... return C.add(self) + self.z
... def mul(self):
... return C.mul(self) * self.z
...
>>> d = D(2, 3, 4)
>>> d.add()
9
>>> d.mul()
24
3. 钻石继承
下面代码中,类 B1 和 类 B2 都是继承自同一个父类 A,而类 C 又同时继承自它们,这种继承模式就被称之为钻石继承,或者菱形继承:
>>> class A:
... def __init__(self):
... print("哈喽,我是A~")
...
>>> class B1(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... B1.__init__(self)
... B2.__init__(self)
... print("哈喽,我是C~")
...
钻石继承这种模式,一旦处理不好就容易带来问题:
>>> c = C()
哈喽,我是A~
哈喽,我是B1~
哈喽,我是A~
哈喽,我是B2~
哈喽,我是C~
4. super() 函数和 MRO 顺序
上面这种通过类名直接访问的做法,是有一个名字的,叫 “调用未绑定的父类方法”。
通常使用其实没有多大问题,但是遇到钻石继承嘛,就容易出事儿了~
那么其实 Python 还有一个更好的实现方案,就是使用 super() 函数。
super() 函数能够在父类中搜索指定的方法,并自动绑定好 self 参数。
>>> class B1(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... super().__init__()
... print("哈喽,我是C~")
...
>>> c = C()
哈喽,我是A~
哈喽,我是B2~
哈喽,我是B1~
哈喽,我是C~
之所以 super() 函数能够有效避免钻石继承带来的问题,是因为它是按照 MRO 顺序去搜索方法,并且自动避免重复调用的问题。
那什么是 MRO 顺序呢?
MRO(Method Resolution Order),翻译过来就是 “方法解析顺序”。
想要查找一个类的 MRO 顺序有两种方法~
一种是通过调用类的 mro() 方法:
>>> C.mro()
[<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A'>, <class 'object'>]
>>> B1.mro()
[<class '__main__.B1'>, <class '__main__.A'>, <class 'object'>]
另一种则是通过 __mro__ 属性:
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A'>, <class 'object'>)
>>> B2.__mro__
(<class '__main__.B2'>, <class '__main__.A'>, <class 'object'>)
注:这里大家会看到它们都有一个
class A:
def __init__(self):
print("I am big man")
class B1(A):
def __init__()
SyntaxError: expected ':'
class B1(A):
def __init__(self):
super().__init__()
print("I am big girl")
class B2(A):
def __init__(self):
super().__init__()
print("we are family")
class C(B1,B2):
def __init__(self):
super().__init__()
print("we are beau")
c = C()
I am big man
we are family
I am big girl
we are beau
class C(B1,B2):
def __init__(self):
B1.__init__(self)
B2.__init__(self)
print("we are beau")
c = C()
I am big man
we are family
I am big girl
I am big man
we are family
we are beau
C.mro()
[, , , , ]
感谢你的耐心!这正是理解 super() 和多重继承时常见的困惑。让我们仔细探讨为什么在调用 B1.__init__() 时会触发 B2.__init__(),这实际上是由于 方法解析顺序(MRO) 的原因。
问题核心:super() 是如何根据 MRO 工作的?
super() 的行为与 Python 中的 方法解析顺序(MRO) 紧密相关,MRO 是决定 Python 如何查找类方法的顺序,尤其在多重继承时。
MRO 是如何确定的?
对于多重继承的类,Python 会通过 C3 线性化算法 来计算 MRO,确保每个类的方法(或构造函数)仅被调用一次,并且顺序是合理的。你可以通过 mro() 方法查看 MRO 的顺序。
对于类 C(B1, B2),MRO 会是:
C -> B1 -> B2 -> A -> object
关键点:super() 依据 MRO 顺序调用父类的构造函数
当我们调用 super() 时,Python 会遵循 MRO 顺序,自动调用下一个类的构造函数,而不是直接调用显式的父类。因此,即使我们在 C 中调用 super(),它并不直接调用 B1.__init__(),而是会跳到 MRO 中下一个类,即 B1。
为什么调用 B1 时会触发 B2?
在类 C 中调用 super().__init__() 时,Python 会根据 MRO 查找下一个类的方法。MRO 的顺序是:
C -> B1 -> B2 -> A
所以,super() 会按照 MRO 顺序首先调用 B1 的构造函数。
在 B1.__init__() 中,我们使用了 super().__init__()。根据 MRO,super() 会继续查找下一个类的方法,即 B2.__init__()(因为 B1 的父类 A 会被跳过,因为它已经出现在 MRO 顺序的后面)。
因此,在 B1 调用 super() 时,实际上是触发了 B2.__init__(),并非直接跳到 A,因为 B2 是在 B1 后面的 MRO 中排在前面的类。
具体流程:
当 C.__init__() 中调用 super().__init__() 时,MRO 会使得调用 B1.__init__()。
在 B1.__init__() 中,调用 super().__init__(),Python 会根据 MRO 查找下一个类。
根据 MRO,super() 会跳到 B2.__init__()(而不是 A.__init__()),并调用 B2.__init__()。
在 B2.__init__() 中,调用 super().__init__(),Python 会继续按照 MRO 顺序找到 A.__init__() 并调用它。
最后,C.__init__() 打印 "we are beau"。
调用链和输出
当 c = C() 被执行时,调用顺序如下:
C.__init__() 调用 super().__init__(),触发 B1.__init__()。
在 B1.__init__() 中,super().__init__() 触发了 B2.__init__()。
在 B2.__init__() 中,super().__init__() 触发了 A.__init__(),并打印 "I am big man"。
然后,B2.__init__() 打印 "we are family"。
接着,B1.__init__() 打印 "I am big girl"。
最后,C.__init__() 打印 "we are beau"。
所以,输出顺序是:
I am big man we are family I am big girl we are beau
结论
super() 会根据 MRO 顺序自动调用父类的构造函数,而不是显式调用某个父类。
在多重继承中,super() 不是简单地调用当前类的直接父类,而是会按照 MRO 顺序查找下一个类的方法。
在这个例子中,C.__init__() 调用 B1.__init__(),然后 B1.__init__() 通过 super().__init__() 调用了 B2.__init__(),而不是直接调用 A.__init__()。
通过以上的逻辑,可以总结出,为啥输出打印的顺序是如上面代码所示,因为按照MRO的顺序,调用完B1,就会紧接调用B2,这就是super()的功能。
这也是为啥改掉最后c = C()里的super()后,改成都是显性的调用,顺序会在B1里执行super()的调用,那时候还是那种mRo的顺序,然后由于显性调用,B2还会被调用,因为B2里和B1一样还是按照MRO顺序,因为里面有super,B2也按顺序走了一遍,最后就变成了结果里的样子了。