繼承中的MRO與super詳解

零下一度
發布: 2017-06-30 13:31:48
原創
1656 人瀏覽過

Python進階-繼承中的MRO與super

寫在前面

#如非特別說明,下文皆基於Python3

摘要
本文敘述Python繼承關係中如何透過super()呼叫「父類別」方法,super(Type, CurrentClass)返回CurrentClassMROType的下一個類別的代理;以及如何設計Python類別以便正確初始化。

1. 單繼承中父類別方法呼叫

在繼承中,呼叫父類別方法是很有必要的。呼叫父類別方法的場景有很多:

  • 例如必須呼叫父類別的建構方法__init__才能正確初始化父類別實例屬性,使得子類別實例物件能夠繼承到父類別實例物件的實例屬性;

  • 再如需要重寫父類別方法時,有時候沒有必要完全摒棄父類別實現,只是在父類別實作前後加一些實現,最終還是要呼叫父類別方法

單繼承是最簡單的繼承關係,多繼承過於複雜,而且使用起來容易出錯。因此有些高階語言完全摒棄了多繼承,只支援單繼承;有些高階語言雖然支援多繼承,但也不建議使用多繼承。 Python也是一樣,在無法完全掌握多繼承時,最好不好使用,單繼承能滿足絕大部分的需求。

1.1 非綁定方式呼叫

綁定方法與非綁定方法的區別與聯繫參見:Python基礎-類

如有以下繼承關係兩個類:

class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
        D.test(self)
登入後複製

現在要求在子類別Ctest函數中呼叫父類別Dtest實作。我們能想到最直接的方法恐怕是直接引用類別物件D的函數成員test了:

class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
登入後複製

嘗試測試一下:

c = C()
c.test()
登入後複製

output:

test in C
test in D
登入後複製

看來非綁定的方式確實滿足了目前呼叫父類別方法的需求。

1.2 builtin 函數super

參考Python tutorial關於super的描述: super(\[type\[, object-or-type\]\])

#Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class. The search order is same as get get as as get ) except that the type itself is skipped.

super函數傳回委託類別type的父類別或兄弟類別方法呼叫的代理物件。 super用來呼叫已經在子類別中重寫了的父類別方法。方法的搜尋順序與getattr()函數相同,只是參數類別type本身被忽略。

1.3 綁定方式呼叫

使用綁定方式呼叫父類別方法,自然不能明確傳入參數目前物件(self)。現在super函數能夠範圍對父類別的代理,因為在單一繼承中子類別有且僅有一個父類,所以父類別是明確的,我們完全清楚呼叫的父類別方法是哪個:

class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')super().test() # super(C, self).test()的省略形式
登入後複製

2. 深入 super

事實上,super函數傳回的代理物件是一個bultin class super,正如它的名字所指,類別super代理了子類別的父類別。在單一繼承關係中,super代理程式的類別很容易找到嗎,就是子類別的唯一父類別;但是在多繼承關係中,super除了能代理子類別的父類別外,還有可能代理子類別的兄弟類別。

2.1 複雜的多重繼承

在多重繼承關係中,繼承關係可能會相當複雜。

class D(object):    def test(self):print('test in D')class C(D):    def test(self):print('test in C')class B(D):    def test(self):print('test in B')class A(B, C):pass
登入後複製

類別A繼承層次結構如下:

  object
    |
    D
   / \
  B   C
   \ /
    A
登入後複製

類別A的繼承關係中存在菱形結構,即可以透過多條路徑從類別A到達某個父類,這裡是D

如果現在要求在類別A中呼叫「父類別」的test方法,需要一種對test方法的搜尋解析順序,來決定到底是呼叫B,C或Dtest方法。

2.2 方法解析順序(MRO)

上面提出的對test的方法的搜尋順序,就是方法解析順序了。

深度優先
Python舊式類別中,方法解析順序是深度優先,多個父類別從左到右。
廣度優先
Python新式類別中,方法解析順序是廣度優先,多個父類別從左到右。

所以上面的解析順序是:A -> B -> C -> D -> object。在

Python中,類別的__mro__屬性展示了方法搜尋順序,可以呼叫mro()方法或直接引用__mro__ 得到搜尋順序:

print(A.mro())print(A.__mro__)
登入後複製

output:

[<class &#39;__main__.A&#39;>, <class &#39;__main__.B&#39;>, <class &#39;__main__.C&#39;>, <class &#39;__main__.D&#39;>, <class &#39;object&#39;>]
(<class &#39;__main__.A&#39;>, <class &#39;__main__.B&#39;>, <class &#39;__main__.C&#39;>, <class &#39;__main__.D&#39;>, <class &#39;object&#39;>)
登入後複製

所以

a = A()
a.test() # output: test in B
登入後複製

变化的MRO
即使是同一个类,在不同的MRO中位置的前后关系都是不同的。如以下类:

class D(object):    def test(self):print(&#39;test in D&#39;)class C(D):    def test(self):print(&#39;test in C&#39;)class B(D):    def test(self):print(&#39;test in B&#39;)
登入後複製

B的继承层次结构为:

  object
    |
    D
   / \
  C   B
登入後複製

B的MRO:B -> D -> object
对比类A的MRO:A -> B -> C -> D -> object
同样的类B,在两个不同的MRO中位置关系也是不同的。可以说,在已有的继承关系中加入新的子类,会在MRO中引入新的类,并且改变解析顺序。

那么可以想象,同样在类B的test中通过super调用父类方法,在不同的MRO中实际调用的方法是不同的。

如下:

class D(object):    def test(self):print(&#39;test in D&#39;)class C(D):    def test(self):print(&#39;test in C&#39;)super().test()class B(D):    def test(self):print(&#39;test in B&#39;)super().test()class A(B, C):passb = B()
b.test()print(&#39;==========&#39;)
a = A()
a.test()
登入後複製

output:

test in B
test in D==========test in B
test in C
test in D
登入後複製

因为在原有的类关系中加入BC的子类A,使得在Btest方法中调用supertest方法发生了改变,原来调用的是其父类Dtest方法,现在调用的是其兄弟类Ctest方法。
从这里可以看出super不总是代理子类的父类,还有可能代理其兄弟类。

因此在设计多继承关系的类体系时,要特别注意这一点。

2.3 再看super方法

方法super([type[, object-or-type]]),返回的是对type的父类或兄弟类的代理。
如果第二个参数省略,返回的super对象是未绑定到确定的MRO上的:

  • 如果第二个参数是对象,那么isinstance(obj, type)必须为True

  • 如果第二个参数是类型,那么issubclass(type2, type)必须为True,即第二个参数类型是第一个参数类型的子类。

super函数的第二个参数存在时,其实现大概如以下:

def super(cls, inst):
    mro = inst.__class__.mro() # Always the most derived classreturn mro[mro.index(cls) + 1]
登入後複製

很明显,super返回在第二个参数对应类的MRO列表中,第一个参数type的下一个类的代理。因此,要求第一个参数type存在于第二个参数类的MRO是必要的,只有第一个参数类是第二个参数所对应类的父类,才能保证。

super()
super函数是要求有参数的,不存在无参的super函数。在类定义中以super()方式调用,是一种省略写法,由解释器填充必要参数。填充的第一个参数是当前类,第二个参数是self

super() => super(current_class, self)
登入後複製

所以,super()这种写法注定只能在类定义中使用。

现在再来看上面的继承关系:

class D(object):def test(self):print(&#39;test in D&#39;)class C(D):def test(self):print(&#39;test in C&#39;)# super().test() # 与下面的写法等价super(C, self).test() # 返回self对应类的MRO中,类C的下一个类的代理class B(D):def test(self):print('test in B')# super().test() # 与下面的写法等价super(B, self).test() # 返回self对应类的MRO中,类B的下一个类的代理class A(B, C):pass
登入後複製

因此:

b = B()
b.test() # 基于类B的MRO(B->D->object),类B中的super()代理Dprint(&#39;==========&#39;)
a = A()
a.test() # 基于类A的MRO(A->B->C->D->object),类B中的super()代理C
登入後複製

以上就是在继承关系中引入新类,改变方法解析顺序的实例。

super([type[, object-or-type]])的第二个参数,对象和类还有一点区别:使用对象返回的是代理使用绑定方法,使用类返回的代理使用非绑定方法。
如:

b = B()super(B, b).test()super(B, B).test(b)
登入後複製

这两种方式得到的结果是相同的,区别在于非绑定调用与绑定调用。

3. 最佳实践

3.1 不可预测的调用

普通的函数或者方法调用中,调用者肯定事先知道被调用者所需的参数,然后可以轻松的组织参数调用。但是在多继承关系中,情况有些尴尬,使用super代理调用方法,编写类的作者并不知道最终会调用哪个类的方法,这个类都可能尚未存在。

如现在一作者编写了以下类:

class D(object):def test(self):print(&#39;test in D&#39;)        
class B(D):def test(self):print(&#39;test in B&#39;)super().test()
登入後複製

在定义类D时,作者完全不可能知道test方法中的super().test()最终会调用到哪个类。
因为如果后来有人在这个类体系的基础上,引入了如下类:

class C(D):def test(self):print(&#39;test in C&#39;)super().test()        
class A(B, C):passa = A()
a.test()
登入後複製

此时会发现类Btest方法中super().test()调用了非原作者编写的类的方法。
这里test方法的参数都是确定的,但是在实际生产中,可能各个类的test方法都是不同的,如果新引入的类C需要不同的参数:

class C(D):def test(self, param_c):print(&#39;test in C, param is&#39;, param_c)super().test()        
class A(B, C):passa = A()
a.test()
登入後複製

B的调用方式调用类Ctest方法肯定会失败,因为没有提供任何参数。类C的作者是不可能去修改类B的实现。那么,如何适应这种参数变换的需求,是在设计Python类中需要考虑的问题。

3.2 实践建议

事实上,这种参数的变换在构造方法上能体现得淋漓尽致,如果子类没有正确初始化父类,那么子类甚至不能从父类继承到需要的实例属性。

所以,Python的类必须设计友好,才能拓展,有以下三条指导原则:

  1. 通过super()调用的方法必须存在;

  2. 调用者和被调用者参数必须匹配;

  3. 所有对父类方法的调用都必须使用super()

3.3 参数匹配

super()代理的类是不可预测的,需要匹配调用者和可能未知的调用者的参数。

固定参数
一种方法是使用位置参数固定函数签名。就像以上使用的test()一样,其签名是固定的,只要要传递固定的参数,总是不会出错。

关键字参数
每个类的构造方法可能需要不同的参数,这时固定参数满足不了这种需求了。幸好,Python中的关键字参数可以满足不定参数的需求。设计函数参数时,参数由关键字参数和关键字参数字典组成,在调用链中,每一个函数获取其所需的关键字参数,保留不需要的参数到**kwargs中,传递到调用链的下一个函数,最终**kwargs为空时,调用调用链中的最后一个函数。

示例:

class Shape(object):def __init__(self, shapename, **kwargs):self.shapename = shapenamesuper().__init__(**kwargs)class ColoredShape(Shape):def __init__(self, color, **kwargs):self.color = colorsuper().__init__(**kwargs)

cs = ColoredShape(color=&#39;red&#39;, shapename=&#39;circle&#39;)
登入後複製

参数的剥落步骤为:

  • 使用cs = ColoredShape(color=&#39;red&#39;, shapename=&#39;circle&#39;)初始化ColoredShape

  • ColoredShape__init__方法获取其需要的关键字参数color,此时的kwargs{shapename:&#39;circle&#39;};

  • 调用调用链中Shape__init__方法,该方法获取所需关键字参数shapename,此时kwargs{};

  • 最后调用调用链末端objet.__init__,此时因为kwargs已经为空。

初始化子类传递的关键字参数尤为重要,如果少传或多传,都会导致初始化不成功。只有MRO中每个类的方法都是用super()来调用“父类”方法时,才能保证super()调用链不会断掉。

3.4 保证方法存在

上面的例子中,由于顶层父类object总是存在__init__方法,在任何MRO链中也总是最后一个,因此任意的super().__init__调用总能保证是object.__init__结束。

但是其他自定义的方法得不到这样的保证。这时需要手动创建类似object的顶层父类:

class Root:def draw(self):# the delegation chain stops hereassert not hasattr(super(), &#39;draw&#39;)class Shape(Root):def __init__(self, shapename, **kwds):self.shapename = shapenamesuper().__init__(**kwds)def draw(self):print(&#39;Drawing.  Setting shape to:&#39;, self.shapename)super().draw()class ColoredShape(Shape):def __init__(self, color, **kwds):self.color = colorsuper().__init__(**kwds)def draw(self):print(&#39;Drawing.  Setting color to:&#39;, self.color)super().draw()

cs = ColoredShape(color=&#39;blue&#39;, shapename=&#39;square&#39;)
cs.draw()
登入後複製

如果有新的类要加入到这个MRO体系,新的子类也要继承Root,这样,所有的对draw()的调用都会经过Root,而不会到达没有draw方法的object了。这种对于子类的扩展要求,应当详细注明在文档中,便于使用者阅读。这种限制与Python所有异常都必须继承自BaseException一样。

3.5 组合不友好的类

对于那些不友好的类:

class Moveable:def __init__(self, x, y):self.x = xself.y = ydef draw(self):print(&#39;Drawing at position:&#39;, self.x, self.y)
登入後複製

如果希望使用它的功能,直接将其加入到我们友好的继承体系中,会破坏原有类的友好性。
除了通过继承获得第三方功能外,还有一种称之为组合的方式,即把第三方类作为组件的方式揉入类中,使得类具有第三方的功能:

class MoveableAdapter(Root):def __init__(self, x, y, **kwds):self.movable = Moveable(x, y)super().__init__(**kwds)def draw(self):self.movable.draw()super().draw()
登入後複製

Moveable被作为组件整合到适配类MoveableAdapter中,适配类拥有了Moveable的功能,而且是友好实现的。完全可以通过继承适配类的方式,将Moveable的功能加入到友好的继承体系中:

class MovableColoredShape(ColoredShape, MoveableAdapter):passMovableColoredShape(color=&#39;red&#39;, shapename=&#39;triangle&#39;,
                    x=10, y=20).draw()
登入後複製

参考

Python’s super() considered super!
Python tutorial#super

以上是繼承中的MRO與super詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板