>  기사  >  백엔드 개발  >  상속의 MRO 및 Super에 대한 자세한 설명

상속의 MRO 및 Super에 대한 자세한 설명

零下一度
零下一度원래의
2017-06-30 13:31:481634검색

Python Advanced - 상속의 MRO 및 Super

앞서 작성

별도의 언급이 없는 한 다음은 Python3을 기반으로 합니다

Abstract
이 문서에서는 Python에서 상속 관계를 전달하는 방법을 설명합니다. > super()는 "상위 클래스" 메서드를 호출하고, super(Type, CurrentClass)CurrentClassMRO를 반환합니다. Type 및 Python 클래스가 올바르게 초기화되도록 설계하는 방법. 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 that used by getattr() 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__

1. 단일 상속에서 부모 클래스 메서드 호출
상속에서는 부모 클래스 메서드를 호출해야 합니다. 상위 클래스 메서드를 호출하는 시나리오는 다양합니다. 🎜
  • 🎜예를 들어, 상위 클래스의 생성자 메서드 __init__를 호출하여 올바르게 초기화해야 합니다. 상위 클래스 인스턴스 속성을 사용하면 하위 클래스 인스턴스 객체가 상위 클래스 인스턴스 객체의 인스턴스 속성을 상속할 수 있습니다. 🎜
  • 🎜상위 클래스 메서드를 재정의해야 하는 경우 때로는 완전히 버릴 필요가 없습니다. 부모 클래스 구현, 그냥 부모 클래스에 구현하기 전후에 구현을 추가한 후에도 여전히 부모 클래스 메서드를 호출해야 합니다🎜
🎜단일 상속이 가장 간단한 상속 관계입니다. 다중 상속도 마찬가지입니다. 사용하기 복잡하고 오류가 발생하기 쉽습니다. 따라서 일부 고급 언어는 다중 상속을 완전히 포기하고 단일 상속만 지원합니다. 일부 고급 언어는 다중 상속을 지원하지만 다중 상속은 권장되지 않습니다. Python도 마찬가지입니다. 다중 상속을 완전히 익힐 수 없으면 단일 상속을 사용하지 않는 것이 가장 좋습니다. 🎜
1.1 비바인딩 모드에서 호출
🎜바운드 메서드와 비바인딩 메서드의 차이점과 연결은 Python 기본 - 클래스를 참조하세요.🎜🎜다음과 같은 상속 관계를 갖는 두 클래스가 있는 경우 :🎜🎜
print(A.mro())print(A.__mro__)
🎜🎜이제 하위 클래스 test 함수에서 상위 클래스 Dtest 구현을 호출해야 합니다. >C. 우리가 생각할 수 있는 가장 직접적인 방법은 아마도 클래스 객체 D의 함수 멤버 test를 직접 참조하는 것입니다: 🎜🎜
[<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
🎜🎜출력: 🎜🎜
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;)
🎜🎜비바인딩 메서드가 상위 클래스 메서드 호출에 대한 현재 요구 사항을 충족하는 것 같습니다. 🎜
1.2 내장 함수 super
🎜super에 대한 Python 튜토리얼의 설명을 참조하세요: super([type[, object-or-type]])🎜🎜🎜프록시 객체 반환 이는 유형의 상위 또는 형제 클래스에 메소드 호출을 위임합니다. 이는 클래스에서 재정의된 상속된 메소드에 액세스하는 데 유용합니다. 검색 순서는 유형 자체를 건너뛴다는 점을 제외하면 getattr()에서 사용하는 것과 동일합니다.🎜 🎜🎜 super 함수는 위임 클래스 type의 상위 클래스 또는 형제 클래스 메서드가 호출한 프록시 객체를 반환합니다. super는 하위 클래스에서 재정의된 상위 클래스 메서드를 호출하는 데 사용됩니다. 메서드 검색 순서는 매개변수 클래스 type 자체가 무시된다는 점을 제외하면 getattr() 함수와 동일합니다. 🎜
1.3 바인딩 메서드 호출
🎜바인딩 메서드를 사용하여 상위 클래스 메서드를 호출하면 당연히 매개변수 현재 개체(self)를 명시적으로 전달할 수 없습니다. 이제 super 함수는 상위 클래스의 프록시 범위를 지정할 수 있습니다. 단일 상속에서 하위 클래스에는 상위 클래스가 하나만 있고 상위 클래스가 하나만 있으므로 상위 클래스가 명확하고 어떤 상위 클래스 메서드가 무엇인지 완전히 알 수 있기 때문입니다. :🎜🎜
  object
    |
    D
   / \
  C   B
🎜🎜2. super에 대해 더 자세히 알아보기🎜🎜실제로 super 함수에 의해 반환된 프록시 객체는 이름에서 알 수 있듯이 bultin 클래스 super입니다. 에서 super 클래스는 하위 클래스의 상위 클래스를 나타냅니다. 단일 상속 관계에서는 super가 나타내는 클래스를 쉽게 찾을 수 있나요? 이 클래스는 하위 클래스의 유일한 상위 클래스이지만 다중 상속 관계에서는 super가 해당 클래스를 찾을 수 있습니다. 프록시 하위 클래스뿐만 아니라 상위 클래스 외에도 하위 클래스의 형제 클래스도 프록시할 수 있습니다. 🎜
2.1 복잡한 다중 상속
🎜다중 상속 관계에서 상속 관계는 상당히 복잡할 수 있습니다. 🎜🎜
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()
🎜🎜 A 클래스의 상속 계층은 다음과 같습니다. 🎜
test in B
test in D==========test in B
test in C
test in D
🎜 A 클래스의 상속 관계에는 다이아몬드 구조가 있습니다. A 클래스의 여러 경로가 특정 상위 클래스(이 경우 D)에 도달합니다. 🎜🎜이제 A 클래스에서 "🎜Parent Class🎜"의 test 메서드를 호출해야 하는 경우 code>test 메소드의 검색 및 파싱 순서에 따라 <code>B, C, Dtest 메소드 호출 여부를 결정합니다. 🎜
2.2 Method Resolution Order(MRO)
🎜위에서 제안한 test의 메소드 검색 순서는 Method Resolution Order입니다. 🎜🎜🎜깊이 우선🎜🎜Python이전 스타일 클래스에서 메서드 구문 분석 순서는 깊이 우선이며 여러 상위 클래스는 왼쪽에서 오른쪽입니다. 🎜🎜너비 우선🎜🎜Python 새 스타일 클래스에서 메서드 확인 순서는 너비 우선이고 여러 상위 클래스는 왼쪽에서 오른쪽입니다. 🎜🎜위의 구문 분석 순서는 A -> B -> C -> D -> object입니다. 🎜🎜Python에서 클래스의 __mro__ 속성은 메서드 검색 순서를 보여줍니다. mro() 메서드를 호출하거나 __mro__검색 순서 가져오기: 🎜🎜
def super(cls, inst):
    mro = inst.__class__.mro() # Always the most derived classreturn mro[mro.index(cls) + 1]
🎜

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

위 내용은 상속의 MRO 및 Super에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.