类中的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()的部分定义如下:

  1. super构造函数参数为:super([type[, > object-or-type]])。object-or-type 的 __ mro__ 属性列出了super() 使用的方法解析搜索顺序。 该属性是动态的,可以在任何继承层级结构发生更新的时候被改变
  2. object-or-type 确定用于搜索的 method resolution order。 搜索会从 type 之后的类开始。 举例来说,如果 object-or-type 的 __ mro__ 为 D -> B -> C -> A -> object 并且 type 的值为 B,则 super() 将会搜索 C -> A -> object。
  3. 如果省略第二个参数,则返回的超类对象是未绑定的。 如果第二个参数为一个对象,则 isinstance(obj, type) 必须为真值。 如果第二个参数为一个类型,则 issubclass(type2, type) 必须为真值(这适用于类方法)。
  4. 在类中调用基类方法和属性时,可以省略参数,编译器会自动填入所需的参数。

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(),或者该类不从根类继承)。通过创建一个按规则运行的适配器类,可以很容易地纠正这种情况。

主要参考整理自:

  1. Python 文档: super()
  2. Super considered super
  3. 一篇关于MRO的中文博客(公式存在错误)
  4. Wiki中对python mro算法的介绍
  5. cnblog
  6. 《Python 大学教程》 张基温