类中的super
什么是super
super本质上是一个类名,而super()是super类的构造方法。super()返回一个super对象, 此对象可用于按照MRO顺序查找并调用类的继承链上的基类方法,常被用于调用被重写的基类方法。
MRO
解释super不得不提到MRO(method resolution order)。
Python是一个支持多继承的语言。派生类可以继承和重写基类方法,这使得派生类和不同的基类可能都具有某个同名方法,同时某个方法或许只属于某个很祖先的基类。因此,在调用对象的方法时,Python需要确定调用具体哪个类的方法。而为了避免二义性,必须给从派生类到各个层级的基类排序,以便确定方法调用的优先级。按照这个顺序,依次查找方法是否存在并调用的过程就是MRO。
Python的MRO算法是"C3"。其具体描述可以在Wikipedia上找到。算法的原则包括:1. 子类排在基类前面;2. 同层次基类按类定义中的继承顺序排序(例如:class Son(Father1, Father2), 从左到右);3. 子类不改变基类的MRO顺序。
1和2都好理解,重点在3。3的“学名”叫单调性原则。一个叫Michele Simionato的人对MRO的单调性定义如下:
A MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C.
翻译过来,就是:若在C的MRO中,C1先于C2,则在C的任何子类的MRO中,C1都必须先于C2。也就是说,在类存在多继承时,子类不能改变基类的 MRO 搜索顺序,否则会导致程序发生异常。
Python 2.3 以前的MRO方法违反了单调性原则。下面以2.2的方法为例说明何为违反单调性原则。
Python 2.2 的MRO采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。
例如,分析如下程序:class X(object): pass class Y(object): pass class A(X,Y): pass class B(Y,X): pass class C(A, B): pass
对于C,通过进行深度遍历,得到搜索顺序为 C->A->X->object->Y->object->B->Y->object->X->object,再进行简化(相同取后者),得到 C->A->B->Y->X->object。 同理,对于 A,其搜索顺序为 A->X->Y->object; 对于 B,其搜索顺序为 B->Y->X->object。可以看到,B 和 C 中,X、Y 的搜索顺序是相反的,也就是说,当 B 被继承时,它本身的搜索顺序发生了改变,这违反了单调性原则。
出现Python 2.3 的新式类以后,这个问题被解决。在Python 3 中,由于所有类都是新式类,因此单调性问题不再存在了。
super()的定义
super()的定义与MRO密不可分。Python文档对super()的部分定义如下:
- super构造函数参数为:super([type[, > object-or-type]])。object-or-type 的 __ mro__ 属性列出了super() 使用的方法解析搜索顺序。 该属性是动态的,可以在任何继承层级结构发生更新的时候被改变。
- object-or-type 确定用于搜索的 method resolution order。 搜索会从 type 之后的类开始。 举例来说,如果 object-or-type 的 __ mro__ 为 D -> B -> C -> A -> object 并且 type 的值为 B,则 super() 将会搜索 C -> A -> object。
- 如果省略第二个参数,则返回的超类对象是未绑定的。 如果第二个参数为一个对象,则 isinstance(obj, type) 必须为真值。 如果第二个参数为一个类型,则 issubclass(type2, type) 必须为真值(这适用于类方法)。
- 在类中调用基类方法和属性时,可以省略参数,编译器会自动填入所需的参数。
super()的用例
文章最前面提到,super的典型应用是调用被重写的基类方法。这种应用看起来是这样的:
class C(B):
def method(self, arg):
super().method(arg)
而调用基类方法可以分为不同的情形。
1. 调用基类初始化函数
当子类重写初始化方法__ init__()时,如果不显式调用父类的初始化函数,就只能初始化子类属性,无法执行父类初始化函数。可以使用super().__ init()__ 调用父类初始化函数。
例如:
class A(B):
def __init(self, arg_A=a, **kwg):
super().__init(**kwg)
self.a = a
上面在参数传递时使用双星号,后文会解释其妙用。
2. 调用被覆盖的父类函数
例如,设计一个类,它继承自dict类,并能在添加key时输出logging信息:
class LoggingDict(dict):
def __setitem__(self, key, value):
logging.info('Setting %r to %r' % (key, value))
super().__setitem__(key, value)
super()的妙处
super对象可以自动在程序运行时解析派生类的MRO,这使得使用super可以带来以下好处:
1. 基于MRO的单调性,使用super可以保证派生类和基类的同名方法都被按照派生类的MRO顺序调用一次,且仅被调用一次。
例如:
class A:
def __init__(self):
print("A initiated!")
class B(A):
def __init__(self):
super().__init__()
print("B initiated!")
class C(A):
def __init__(self):
super().__init__()
print("C initiated!")
class D(C, B):
def __init__(self):
super().__init__()
print("D initiated!")
class E(C, B):
def __init__(self):
super().__init__()
print("E initiated!")
class F(E,D):
def __init__(self):
super().__init__()
print("F initiated!")
f = F()
输出:
A initiated!
B initiated!
C initiated!
D initiated!
E initiated!
F initiated!
在多继承情况下,倘若不使用super, 直接使用基类名称调用函数,则会出现下面的情况:
class A:
def __init__(self):
print("A initiated!")
class B(A):
def __init__(self):
print("B initiated!")
#super().__init__()
A.__init__(self)
class C(A):
def __init__(self):
print("C initiated!")
#super().__init__()
A.__init__(self)
class D(C, B):
def __init__(self):
print("D initiated!")
#super().__init__()
B.__init__(self)
C.__init__(self)
class E(C, B):
def __init__(self):
print("E initiated!")
#super().__init__()
C.__init__(self)
B.__init__(self)
class F(E, D):
def __init__(self):
print("F initiated!")
#super().__init__()
E.__init__(self)
D.__init__(self)
f = F()
输出:
F initiated!
E initiated!
C initiated!
A initiated!
B initiated!
A initiated!
D initiated!
B initiated!
A initiated!
C initiated!
A initiated!
即基类函数没有按照MRO顺序被依次调用,而是沿着错综复杂的继承网络被调用多次,抑或是因为写代码时疏忽漏掉了某个基类函数,从而可能引发错误和性能损失。
2. 当派生类的基类发生改变时,如果派生类调用基类方法的的方式是super(),那么派生类的代码就不需要修改。(前提是改变后的基类仍有对应的方法)。
仍以扩展dict类为例:
class LoggingDict(SomeOtherClass): # new base class
def __setitem__(self, key, value):
logging.info('Setting %r to %r' % (key, value))
super().__setitem__(key, value) # no change needed
上面的例子中,LoggingDict的基类变成了其他类,但是由于使用了super(), super对象会自动调用改变后的MRO上的方法,无需改动代码。
3. 通过使用super,可以仅重新组合基类就可以实现更强大的功能。例如:
class LoggingOD(LoggingDict, collections.OrderedDict):
pass
在之前的例子中,LoggingDict继承自dict类,拓展出dict的Logging功能。而这个例子中,LoggingOD通过继承LoggingDict和collections.OrderedDict(dict的派生类),形成了一个这样的MRO: LoggingOD, LoggingDict, OrderedDict, dict, object。在这条链路中,OrderedDict被插到LoggingDict和dict中间,使得LoggingDict的super().setitem(key, val)被委派给OrderedDict,而LoggingOD又继承了LoggingDict的setitem方法,从而以几乎不写代码的方式拓展了OrderedDict的logging功能。
如何定义能更好使用super()的类
1. 使用keyword arguments
派生类和基类的同名函数的参数可能不同:参数类型不同,位置不同,数量不同。使用keyword arguments可以让各层函数自行取用所需的参数,将剩余参数传递给下一个层级使用。例如:
class A:
def __init__(self, a=0,**kwag):
print("A initiated!")
print(kwag)
self.a = a
class B(A):
def __init__(self, b=0, **kwag):
super().__init__(**kwag)
print("B initiated!")
print(kwag)
self.b = b
class C(A):
def __init__(self,c=0, **kwag):
super().__init__(**kwag)
print("C initiated!")
print(kwag)
self.c = c
class D(C, B):
def __init__(self, d=0,**kwag):
super().__init__(**kwag)
print("D initiated!")
print(kwag)
self.d = d
class E(C, B):
def __init__(self, e=0, t=0, **kwag):
super().__init__(**kwag)
print("E initiated!")
print(kwag)
self.e = e
self.t = t
class F(E, D):
def __init__(self, f=0, **kwag):
super().__init__(**kwag)
print("F initiated!")
print(kwag)
self.f = f
f = F(a=1,b=2,c=3,d=4,e=5,f=6, t=88)
print(f.a)
输出:
A initiated!
{}
B initiated!
{'a': 1}
C initiated!
{'a': 1, 'b': 2}
D initiated!
{'a': 1, 'b': 2, 'c': 3}
E initiated!
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
F initiated!
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 't': 88}
1
2. 设置一个根类,使派生类MRO上所有自定义类都继承于她。这个根类吃进去所有派生类的super函数的劫余, 且不再使用super调用上层的方法。这可以确保派生类的super().__ base_method()__调用不会出现 attribute error。
3. 有时,子类可能希望将协作多重继承技术用于不是为其设计的第三方类(可能其感兴趣的方法不使用super(),或者该类不从根类继承)。通过创建一个按规则运行的适配器类,可以很容易地纠正这种情况。
主要参考整理自:
- Python 文档: super()
- Super considered super
- 一篇关于MRO的中文博客(公式存在错误)
- Wiki中对python mro算法的介绍
- cnblog
- 《Python 大学教程》 张基温