Python中super()函数的用法及其说明
为了调用父类(超类)的一个方法,可以使用super()
函数,比如:
1 | class A: |
super()
函数的一个常见用法是在__init__()
中确保父类被正确的初始化:
1 | class A: |
说明__init__()
函数:定义类的时候,若是添加__init__()
函数,那么在创建类的实例的时候,实例会自动调用这个方法,一般用来对实例的属性进行初始化。
super()
的另外一个常见用法出现在覆盖Python特殊方法的代码中,比如:
1 | class Proxy: |
实际上,大家对于在Python中如何正确使用super()
函数普遍知之甚少。你有时候会看到像下面这样直接调用父类的一个方法:
1 | class Base: |
尽管对于大部分代码而言这么做没什么问题,但是在更复杂的涉及到多继承的代码中就有可能导致很奇怪的问题发生。比如,考虑下面的情况:
1 | class Base: |
运行这段代码后就会发现Base.__init__
被调用两次,如图所示:
1 | c = C() |
可能两次调用Base.__init__()
没什么坏处,但有时候却不是。另一方面,假设在代码中换成使用super()
,结果就很完美了:
1 | class Base: |
运行这个新版本后,你会发现Base.__init__()
方法只会被调用一次:
1 | c = C() |
所以说,super()
是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。
说明:MRO就是类的方法解析顺序表,其实也就是继承父类方法时的顺序表,下面会有更详尽的介绍。
为了弄清它的原理,我们需要花点时间解释下Python是如何实现继承的。对于你定义的每一个类,Python会计算出一个所谓的方法解析顺序(MRO)列表。这个MRO列表就是一个简单的所有基类的线性顺序表。例如:
1 | C.__mro__ |
为了实现继承,Python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
老实说,你所要知道的就是MRO列表中的类顺序会让你定义的任意类层级关系变得有意义。
当你使用super()
函数时,Python会在MRO列表上继续搜索下一个类。只要每个重定义的方法统一使用super()
并只调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次。这也是为什么在第二个例子中你不会调用两次Base.__init__()
的原因。
super()
有个令人吃惊的地方是它并不一定去查找某个类在MRO中下一个直接父类,你甚至可以在一个没有直接父类的类中使用它。例如,考虑如下这个类:
1 | class A: |
如果你试着直接使用这个类就会出错:
1 | a = A() |
但是,如果你使用多继承的话看看会发生什么:
1 | class B: |
你可以看到在类A中使用super().spam()
实际上调用的是与类A毫无关系的类B中的spam()
方法。这个用类C的MRO列表就可以完全解释清楚:
1 | C.__mro__ |
在定义混入类的时候这样使用super()
是很普遍的。
然而,由于super()
可能会调用不是你想要的方法,你应该遵循一些通用原则。首先,确保在继承体系中所有相同名字的方法拥有可兼容的参数签名(比如相同的参数个数和参数名称)。这样可以确保super()
调用一个非直接父类方法时不会出错。其次,最好确保最顶层的类提供了这个方法的实现,这样的话在MRO上面的查找链肯定可以找到某个确定的方法。
在Python社区内对于super()
的使用有时候会引来一些争议。尽管如此,如果一切顺利的话,你应该在你最新的代码中使用它。Raymond Hettinger为此写了一篇非常好的文章,有兴趣的话可以去查查看,文章通过大量的例子向我们解释了为什么super()
是极好的。
最后通过一个很好的实例帮助大家加深一下记忆:
1 | class FooParent(object): |
执行结果:
1 | Parent |
引用文章: