揭开Python super()的真面目
揭开Python super()的真面目
蔡坨坨转载请注明出处❤️
作者:测试蔡坨坨
原文链接:caituotuo.top/51596e53.html
你好,我是测试蔡坨坨。
今天,我们来介绍一下Python中的super()。
相信大多数人对super()的使用可能就是有一个class,比如Boy,然后继承另外一个class,比如Person,然后在Boy里面,也就是它的子类__init__
函数里面用super().__init__()
来调用它父类的初始化函数。可能这个就是很多人掌握super()的全部知识了,所以这篇文章争取让大家更加详细和深入的了解一下super这个东西。
1 | from objprint import op |
首先介绍一个冷知识,很多人会告诉你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 | from objprint import op |
我们又给这段代码增加一层继承的层级,Person也属于Animal,而所有Animal都有年龄age。
super(Boy, self).__init__(age, name)
就会正常的初始化所有的东西,它会调Person的__init__
,然后Person的__init__
会调Animal的__init__
,最后就完成了Boy。
1 | class Animal: |
如果把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 | from objprint import op |
我们再往下讲,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 | class 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 | class A: |
讲到这里是不是发现自己对super的理解实际上是有偏差的,大部分人都会把super理解为调用父类的同名函数,其实它并不是那么简单,本篇对super的介绍就到这里,希望对大家有所帮助。