Heim  >  Artikel  >  Backend-Entwicklung  >  Detaillierte Erklärung von MRO und Super in der Vererbung

Detaillierte Erklärung von MRO und Super in der Vererbung

零下一度
零下一度Original
2017-06-30 13:31:481642Durchsuche

Python Advanced - MRO und Super in der Vererbung

Vorab geschrieben

Sofern nicht anders angegeben, basiert das Folgende auf Python3

Zusammenfassung
In diesem Artikel wird beschrieben, wie die Methode „Elternklasse“ über Python in der super() Vererbungsbeziehung aufgerufen wird und super(Type, CurrentClass) den Proxy der nächsten Klasse in CurrentClass von ;Und wie man die Klasse MRO so gestaltet, dass sie korrekt initialisiert wird. TypePython1. Aufrufen von Methoden der übergeordneten Klasse bei der Einzelvererbung

Bei der Vererbung ist es notwendig, Methoden der übergeordneten Klasse aufzurufen. Es gibt viele Szenarien zum Aufrufen von Methoden der übergeordneten Klasse:

    Zum Beispiel muss die Konstruktormethode der übergeordneten Klasse
  • aufgerufen werden, um die Instanzattribute der übergeordneten Klasse korrekt zu initialisieren, damit die Das Instanzobjekt der Unterklasse kann von der übergeordneten Klasse erben.

    __init__

  • Ein weiteres Beispiel ist, wenn Sie die Methode der übergeordneten Klasse überschreiben müssen Sie fügen einfach einige Implementierungen vor und nach der Implementierung der übergeordneten Klasse hinzu. Am Ende ist es immer noch so, dass die Methode der übergeordneten Klasse
  • Einzelne Vererbung ist die einfachste Vererbungsbeziehung. Die Mehrfachvererbung ist zu kompliziert und fehleranfällig. Daher verzichten einige Hochsprachen vollständig auf die Mehrfachvererbung und unterstützen nur die Einzelvererbung. Obwohl einige Hochsprachen die Mehrfachvererbung unterstützen, wird die Mehrfachvererbung nicht empfohlen.
Das Gleiche gilt. Wenn Sie die Mehrfachvererbung nicht vollständig verstehen, ist es am besten, sie nicht zu verwenden. Die Einzelvererbung kann die meisten Anforderungen erfüllen.

Python1.1 Aufruf im unverbindlichen Modus

Unterschiede und Zusammenhänge zwischen gebundenen Methoden und unverbindlichen Methoden, siehe: Python-Grundlagen – Klassen

Wenn es zwei Klassen mit dem gibt Folgende Vererbungsbeziehung:

class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
        D.test(self)
erfordert nun den Aufruf der
Implementierung der übergeordneten Klasse

in der C Funktion der Unterklasse test. Die direkteste Methode, die wir uns vorstellen können, besteht wahrscheinlich darin, direkt auf das Funktionsmitglied D des Klassenobjekts test zu verweisen: Dtest

class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
Versuchen Sie es zu testen:

c = C()
c.test()
Ausgabe:

test in C
test in D
Es scheint, dass die unverbindliche Methode die aktuellen Anforderungen für den Aufruf von übergeordneten Klassenmethoden erfüllt.

1.2 integrierte Funktion super

Siehe die Beschreibung von super im Python-Tutorial:

super([type[, object-or-type]])

Gibt ein Proxy-Objekt zurück, das Methodenaufrufe an ein übergeordnetes oder gleichgeordnetes Objekt delegiert Klasse des Typs. Dies ist nützlich für den Zugriff auf geerbte Methoden, die in einer Klasse überschrieben wurden. Die Suchreihenfolge ist dieselbe wie die von getattr() verwendete, außer dass der Typ selbst übersprungen wird.

Die Funktion gibt das Proxy-Objekt zurück, das von der übergeordneten Klasse oder Geschwisterklassenmethode der Delegate-Klasse aufgerufen wird

. superWird zum Aufrufen von Methoden der übergeordneten Klasse verwendet, die in Unterklassen überschrieben wurden. Die Suchreihenfolge für Methoden ist dieselbe wie für type-Funktionen, außer dass die Parameterklasse super selbst ignoriert wird. getattr()type1.3 Aufruf im Bindungsmodus

Beim Aufruf der übergeordneten Klassenmethode im Bindungsmodus kann der Parameter aktuelles Objekt (
) natürlich nicht explizit übergeben werden. Jetzt kann die Funktion

den Proxy der übergeordneten Klasse festlegen, da die Unterklasse bei der Einzelvererbung nur eine übergeordnete Klasse hat, sodass die übergeordnete Klasse klar ist und wir vollständig wissen, welche Methode der übergeordneten Klasse aufgerufen wird: 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. Ein tiefer Einblick in Super

Tatsächlich ist das von der

-Funktion zurückgegebene Proxy-Objekt ein

, wie der Name schon sagt Klasse super Proxy Die übergeordnete Klasse der Unterklasse. Ist es in einer Einzelvererbungsbeziehung einfach, die Klasse zu finden, die bultin class super darstellt? Es ist jedoch in einer Mehrfachvererbungsbeziehung zusätzlich zur übergeordneten Klasse der Unterklasse möglich stellt auch die Unterklasse dar. supersuper2.1 Komplexe Mehrfachvererbung super

Bei Mehrfacherbbeziehungen kann die Erbschaftsbeziehung recht komplex sein.

Klasse
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
Vererbungshierarchie ist wie folgt:

AEs gibt eine Rautenstruktur in der Vererbungsbeziehung der Klasse

Das heißt, es können mehrere Zeilen übergeben werden. Der Pfad führt von der Klasse
  object
    |
    D
   / \
  B   C
   \ /
    A
zu einer übergeordneten Klasse, hier ist er

. AAWenn Sie nun die D-Methode von „

Elternklasse A“ in der Klasse aufrufen müssen, benötigen Sie eine Such- und Parsingreihenfolge dafür -Methode, um zu entscheiden, ob die -Methode von test aufgerufen werden soll. testB,C或D2.2 Methodenauflösungsreihenfolge (MRO) test

Die oben vorgeschlagene Suchreihenfolge für Methoden von
ist die Methodenauflösungsreihenfolge.

testTiefe zuerst

In Klassen im alten Stil ist die Reihenfolge der Methodenauflösung die Tiefe zuerst, mit mehreren übergeordneten Klassen von links nach rechts.
Breite zuerst Python
In Klassen neuen Stils erfolgt die Reihenfolge der Methodenauflösung nach der Breite, mit mehreren übergeordneten Klassen von links nach rechts.
Die obige Parsing-Reihenfolge lautet also: Python. In

A -> B -> C -> D -> object zeigt das

-Attribut der Klasse die Suchreihenfolge der Methode an. Sie können die Methode

aufrufen oder direkt Python zitieren, um die Suchreihenfolge zu erhalten: __mro__mro()__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='red', shapename='circle')初始化ColoredShape

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

  • 调用调用链中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

Das obige ist der detaillierte Inhalt vonDetaillierte Erklärung von MRO und Super in der Vererbung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn