Python进阶-继承中的MRO与super

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介:

摘要
本文讲述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')

现在要求在子类Ctest函数中调用父类Dtest实现。我们能想到最直接的方法恐怕是直接引用类对象D的函数成员test了:

class D(object):
    def test(self):
        print('test in D')class C(D):
    def test(self):
        print('test in C')
        D.test(self)

尝试测试一下:

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__得到搜索顺序:

print(A.mro())print(A.__mro__)

output:

[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>](<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)

所以

a = A()
a.test() # output: test in B

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

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')

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('test in D')class C(D):
    
    def test(self):
        print('test in C')        super().test()class B(D):
    
    def test(self):
        print('test in B')        super().test()class A(B, C):
    passb = B()
b.test()print('==========')
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 class
    return 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('test in D')class C(D):
    def test(self):
        print('test in C')        # 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('==========')
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('test in D')        
class B(D):
    def test(self):
        print('test in B')        super().test()

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

class C(D):
    def test(self):
        print('test in C')        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('test in C, param is', 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 = shapename        super().__init__(**kwargs)class ColoredShape(Shape):
    def __init__(self, color, **kwargs):
        self.color = color        super().__init__(**kwargs)

cs = ColoredShape(color='red', shapename='circle')

参数的剥落步骤为:

  • 使用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 here
        assert not hasattr(super(), 'draw')class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename        super().__init__(**kwds)    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)        super().draw()class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color        super().__init__(**kwds)    def draw(self):
        print('Drawing.  Setting color to:', self.color)        super().draw()

cs = ColoredShape(color='blue', shapename='square')
cs.draw()

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

3.5 组合不友好的类

对于那些不友好的类:

class Moveable:
    def __init__(self, x, y):
        self.x = x        self.y = y    def draw(self):
        print('Drawing at position:', 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的功能加入到友好的继承体系中:

 

                    x=10, y=20).draw()


本文转自lzwxx 51CTO博客,原文链接:
http://blog.51cto.com/13064681/1943439

相关文章
|
3月前
|
算法 Python
python多继承的3C算法是什么?怎么用?
有很多地方都说python多继承的继承顺序,是按照深度遍历的方式,其实python多继承顺序的算法,不是严格意义上的深度遍历,而是基于深度遍历基础上优化出一种叫3C算法
|
4月前
|
数据采集 网络协议 数据挖掘
网络爬虫进阶之路:深入理解HTTP协议,用Python urllib解锁新技能
【7月更文挑战第30天】网络爬虫是数据分析和信息聚合的关键工具。深入理解HTTP协议及掌握Python的urllib库对于高效爬虫开发至关重要。HTTP协议采用请求/响应模型,具有无状态性、支持多种请求方法和内容协商等特点。
48 3
|
3月前
|
算法 Python
Python 中的继承:强大的代码复用机制
【8月更文挑战第23天】
44 0
|
4月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
【7月更文挑战第25天】在网络技术快速发展的背景下, Python因其简洁的语法和强大的库支持成为学习网络编程的理想选择。
67 5
|
4月前
|
机器学习/深度学习 数据采集 算法
Python编程语言进阶学习:深入探索与高级应用
【7月更文挑战第23天】Python的进阶学习是一个不断探索和实践的过程。通过深入学习高级数据结构、面向对象编程、并发编程、性能优化以及在实际项目中的应用,你将能够更加熟练地运用Python解决复杂问题,并在编程道路上走得更远。记住,理论知识只是基础,真正的成长来自于不断的实践和反思。
|
4月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
【7月更文挑战第26天】在网络的数字宇宙中,Python Socket编程是开启网络世界大门的钥匙。本指南将引领你从基础到实战,成为网络世界的建筑师。
62 2
|
4月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
【7月更文挑战第25天】在网络应用蓬勃发展的数字时代,Python凭借其简洁的语法和强大的库支持成为开发高效应用的首选。本文通过实时聊天室案例,介绍了Python Socket编程的基础与进阶技巧,包括服务器与客户端的建立、数据交换等基础篇内容,以及使用多线程和异步IO提升性能的进阶篇。基础示例展示了服务器端监听连接请求、接收转发消息,客户端连接服务器并收发消息的过程。进阶部分讨论了如何利用Python的`threading`模块和`asyncio`库来处理多客户端连接,提高应用的并发处理能力和响应速度。掌握这些技能,能使开发者在网络编程领域更加游刃有余,构建出高性能的应用程序。
31 3
|
4月前
|
SQL 安全 Go
SQL注入不可怕,XSS也不难防!Python Web安全进阶教程,让你安心做开发!
【7月更文挑战第26天】在 Web 开发中, SQL 注入与 XSS 攻击常令人担忧, 但掌握正确防御策略可化解风险. 对抗 SQL 注入的核心是避免直接拼接用户输入至 SQL 语句. 使用 Python 的参数化查询 (如 sqlite3 库) 和 ORM 框架 (如 Django, SQLAlchemy) 可有效防范. 防范 XSS 攻击需严格过滤及转义用户输入. 利用 Django 模板引擎自动转义功能, 或手动转义及设置内容安全策略 (CSP) 来增强防护. 掌握这些技巧, 让你在 Python Web 开发中更加安心. 安全是个持续学习的过程, 不断提升才能有效保护应用.
53 1
|
4月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
【7月更文挑战第12天】归并排序是高效稳定的排序算法,采用分治策略。Python 实现包括递归地分割数组及合并已排序部分。示例代码展示了如何将 `[12, 11, 13, 5, 6]` 分割并归并成有序数组 `[5, 6, 11, 12, 13]`。虽然 $O(n log n)$ 时间复杂度优秀,但需额外空间,适合大规模数据排序。对于小规模数据,可考虑其他算法。**
75 4
|
4月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
107 2