揭开Python super()的真面目

转载请注明出处❤️

作者:测试蔡坨坨

原文链接:caituotuo.top/51596e53.html


你好,我是测试蔡坨坨。

今天,我们来介绍一下Python中的super()。

相信大多数人对super()的使用可能就是有一个class,比如Boy,然后继承另外一个class,比如Person,然后在Boy里面,也就是它的子类__init__函数里面用super().__init__()来调用它父类的初始化函数。可能这个就是很多人掌握super()的全部知识了,所以这篇文章争取让大家更加详细和深入的了解一下super这个东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from objprint import op


class Person:
def __init__(self, name):
self.name = name


class Boy(Person):
def __init__(self, name):
super().__init__(name)
self.gender = "boy"


if __name__ == '__main__':
b = Boy("测试蔡坨坨")
op(b)

首先介绍一个冷知识,很多人会告诉你super是一个方法或者是一个函数,甚至还有人说super是一个关键字,其实都不是,实际上super是一个正儿八经的class,我们可以通过type(super)得知它是一个class,从源码中也可以看出,它是一个内置类的名字,super()并不是调用了一个函数,而是建立了一个super的对象。

尽管我们更常用的形式是super(),括号里面什么都没有,但是super的完整版它的里面应该是有两个参数的,第一个参数是一个type,也就是一个class,而第二个参数是一个type或者是一个object,其中第二个参数决定了这个函数bind到哪一个object或者class上,同时第二个参数决定了使用哪个MRO(所谓的MRO,就是方法解析顺序(Method Resolution Order)),而第一个参数决定了在MRO链上从哪个class开始往后找,这么说比较抽象,我们把它实例化一下就是super(Boy, self).__init__(name)等价于super().__init__(name)

那么这个super(Boy, self)做了什么事呢,首先它要从self这个object里面拿到MRO,这里的self的MRO就是Boy - Person - object,然后它会找到第一个argument,也就是Boy在MRO里所处的位置,在当前情况下,Boy就是最开始的那个,接下来它会从Boy后面的那个class开始找,那它第一个找到的就是Person,然后它就看Person里面有没有__init__这个函数,发现有__init__函数,然后它再把这个__init__函数给bind到self上,这里可以理解为Person的__init__函数传进去的这个self就是super里面的self,也就是说super(Boy, self).__init__(name)等价于Person.__init__(self, name)

那么我们为什么不直接使用Person.__init__(self, name),主要有以下几个原因:

  • 未来有可能改变基类的名字,甚至会改变继承的方式,比如未来我们把这个class Person改名为Human,这个情况下如果我用super,它就什么都不用管,因为它会自动追随着这个MRO,找到正确的class,但是如果用这种命名的class的话,就需要全部修改,这样更容易引入错误。
  • super其实是动态的,它会根据self的MRO进行寻找,而self也就是传进来的这个argument它本身是动态的,也就是说同样一个函数里面,用super在不改变这个函数的情况下,有可能拿到不同的class。这个我们后面细讲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from objprint import op


class Person:
def __init__(self, name):
self.name = name


class Boy(Person):
def __init__(self, name):
# super().__init__(name)
# super(Boy, self).__init__(name)
Person.__init__(self, name)
self.gender = "boy"


if __name__ == '__main__':
b = Boy("测试蔡坨坨")
op(b)

"""
<Boy 0x22f89bdb820
.gender = 'boy',
.name = '测试蔡坨坨'
>
"""

我们又给这段代码增加一层继承的层级,Person也属于Animal,而所有Animal都有年龄age。

super(Boy, self).__init__(age, name)就会正常的初始化所有的东西,它会调Person的__init__,然后Person的__init__会调Animal的__init__,最后就完成了Boy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal:
def __init__(self, age):
self.age = age


class Person(Animal):
def __init__(self, age, name):
super().__init__(age)
self.name = name


class Boy(Person):
def __init__(self, age, name):
# super().__init__(name)
# super(Boy, self).__init__(age, name)
# super(Person, self).__init__(age, name)
super(Person, self).__init__(age)
# Person.__init__(self, name)
self.gender = "boy"


if __name__ == '__main__':
b = Boy(18, "测试蔡坨坨")
op(b)

如果把super(Boy, self).__init__(age, name)改成super(Person, self).__init__(age, name),它就会报错,因为当我们使用super(Person, self)的时候self的MRO链是Boy - Person - Animal - object,第一个argument由于是Person,所以它会从Person后面的那个class,也就是Animal开始找,Animal是有__init__函数的,但是Animal的__init__函数只有一个age,所以当我们传入age、name的时候就错了。

如果我们只传进入age的话,就不会报错,就初始化了一个只有age和gender的Boy object。因为我们跳过了Person的__init__函数。

举这个例子是想跟大家说明super的这两个argument,也就是第一个type和第二个type或者object分别决定了什么。第二个是决定使用这个函数的对象和MRO,而第一个只决定了在MRO这个链上从哪开始找。

super的完全体形态大家一定要记住,因为你只有理解了它的完全体形态,你才会在任何时刻都知道它到底在干什么,比如说super(Boy, b).__init__(20, "蔡坨坨")就可以把这个b object给完整的初始化,你会发现super这个东西并不是只能在class里面使用,它可以在任何地方使用,只要给定第二个参数object或者class,再给定第一个参数从哪开始找,就能使用它的函数。这里就是从b这个object的MRO上,寻找Boy后面开始的__init__函数,这样实际上就找到了Person的__init__函数,然后再用Person的__init__函数对b这个object做初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from objprint import op

class Animal:
def __init__(self, age):
self.age = age

class Person(Animal):
def __init__(self, age, name):
super().__init__(age)
self.name = name

class Boy(Person):
def __init__(self, age, name):
# super().__init__(name)
# super(Boy, self).__init__(age, name)
# super(Person, self).__init__(age, name)
super(Person, self).__init__(age)
# Person.__init__(self, name)
self.gender = "boy"

if __name__ == '__main__':
b = Boy(18, "测试蔡坨坨")
super(Boy, b).__init__(20, "蔡坨坨")
op(b)

"""
<Boy 0x1da117dbdc0
.age = 20,
.gender = 'boy',
.name = '蔡坨坨'
>
"""

我们再往下讲,super的两个参数都是可选参数,当你只给super一个参数的时候,它返回的是一个unbound object,你需要把它bind到一个别的object上才能正确的使用,这个用法相对罕见,一般是比较进阶的编程才会用得到。

那我们平时接触到的最常见的用法其实就是super(),括号里面什么都不给,这种用法是必须要在class的definition里面进行的,因为super用了一些Python的黑魔法做了两件事,第一它会寻找自己是在哪个class里面被定义的,然后把这个class放到它的第一个参数里面,第二它会寻找自己是在哪个函数里面被定义的,然后把这个函数的第一个argument放到它的第二个参数里面,而在当前情况下super()就等价于super(Boy, self),因为它是在Boy这个class里面被定义的,__init__这个函数第一个argument是self。这里就只要记住这个关系就可以啦,至于什么是黑魔法,由于时间关系,今天我们就不去做具体分析。

我们在上面留了一个悬念,我们说super这个东西由于Python dynamic的特性,同样的函数定义,在针对不同的object的时候,会产生不同的效果,在这里举个栗子:

我们看以下的代码非常简单,class A有一个函数say,然后就是打印A,B继承A,B的say函数就是super().say(),然后M继承B,M的say函数也是super().say(),那我们建立了一个m的object,然后用m.say(),这里就非常直观,M找到B的say,然后B再找到A的say,最后打印了一个A。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A:
def say(self):
print("A")


class B(A):
def say(self):
super().say()


class M(B):
def say(self):
super().say()


m = M()
m.say()

# A

我们在MBA完全没有改变的情况下,我们引入一个新的class C,C继承A,C也有一个say函数,这个say函数打印print(“C”),接下来让我们的class M在继承B的同时还继承C,现在的MRO应该是(M)BCA,也就是当M调用super.say()的时候,调用的应该是class B这个say函数,理论上应该打印A,但是实际上并没有打印A,而是打印C,甚至可以把M里面的super().say()改成B.say(self),显性地调用它的基类B的say函数,可以看到结果依然是打印C。

这个时候你就会发现一个非常神奇的事情了,就是B这个class本身跟C没有任何关系,它继承的是A,然而在这个class里面用super却找到C的函数。原因是因为class B里面的super()等价于super(B, self),而寻找方法的顺序是有self的MRO决定的,这个self是一个M object,因此在寻找的时候B后面紧跟着的是C而不是A,这也就出现了在class B里面使用super调用的却不是一个class的父类的函数这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A:
def say(self):
print("A")


class B(A):
def say(self):
super(B, self).say()


class C(A):
def say(self):
print("C")


class M(B, C):
def say(self):
# super().say()
B.say(self)


m = M()
m.say()

讲到这里是不是发现自己对super的理解实际上是有偏差的,大部分人都会把super理解为调用父类的同名函数,其实它并不是那么简单,本篇对super的介绍就到这里,希望对大家有所帮助。