30、Python之面向对象:多继承、菱形继承,不建议用但应该了解

引言

在面向对象的编程语言中,有一个稍微复杂且较少被使用的特性,那就是多继承。在计算机专业的考试中,可能会较多出现多继承的考题,因为它够复杂。实际应用中,除了一些特殊的场景,很少会用到多继承。

但是,在笔者看来,学习Python,还是有必要了解一下Python中的多继承的概念,从而更好地理解相关特性背后的设计理念,在阅读相关源码中,遇到多继承不至于手足无措。

今天的文章,接着上一篇文章,继续聊一下Python中面向对象的第二个特性,也就是继承中的多继承的部分。

编程语言关于多继承的设计实现

多继承的概念并不是Python所独有的,其他面向对象的编程语言中,也同样有多继承的设计实现,只是具体的设计、实现方式会有所不同。以C++、Java和Python为例,简单看一下关于多继承的不同设计理念。

首先,C++中是支持多继承的,也就是一个类可以继承自多个父类。但是,不可避免的会遇到所谓的“菱形继承问题(Diamond Problem)”,即一个类继承自两个类,而这两个类又同时继承自同一个类。这样可能会导致基类的属性和方法被多次调用或者初始化。C++中通过所谓“虚继承(Virtual Inheritance)”来解决这个问题。

其次,Java是明确不支持多继承的语法的,主要是多继承的引入确实会导致程序变得更加复杂,且很多时候滥用多继承会导致逻辑混乱、难以理解。但是,Java中通过区分继承和实现来间接支撑了近似多继承的概念。Java中一个类只能继承自一个类,而不是多个。但是,Java中提供了使用接口以定义功能的特性。一个类虽然只能继承自一个类,但是,可以同时实现多个接口,以扩展类的功能,从结果来看,间接实现了多继承的效果,而且设计上更加符合逻辑。

然后,就是我们现阶段的主角Python了,Python中支持多继承,虽然会使程序的复杂性提升,但是通过MRO(方法解析,前文已经稍微提及)和super内置类,确保多继承的方法解析变得更加明确、一致、可预测,从而简化对多继承代码的理解。

简单介绍了不同语言关于多继承的设计,接下来,回到今天的主题,Python中多继承的介绍。

Python的多继承

首先通过代码实例,来简单看一下Python中多继承的语法,假设有这样一个简化的设计:

1、我们已有的打工人类,体现社会、职场中的人的属性、方法;

2、我们又定义了一个自然人的类,更多的体会人在自然中的属性和方法;

3、程序员应该同时具有打工人和自然人的特性,也就是程序员同时具备打工人和自然人的双重属性,是一种既是又是的关系。

代码实现:

# 定义打工人
class DaGongRen:
    def __init__(self, name, skill):
        self.name = name
        self.skill = skill

    def work(self):
        print(f"打工人{self.name}使用技能【{self.skill}】进行工作")


# 定义自然人
class ZiRanRen:
    def __init__(self, gender, dao):
        self.gender = gender
        self.dao = dao

    def find_dao(self):
        print(f"自然人{self.name}追寻自己的道【{self.dao}】")


# 程序员同时是打工人和自然人
class Programmer(DaGongRen, ZiRanRen):
    def __init__(self, name, skill, gender, dao):
        DaGongRen.__init__(self, name, skill)
        ZiRanRen.__init__(self, gender, dao)


if __name__ == '__main__':
    zs = Programmer('张三', 'Java', '女', '编程之美')
    print(zs.__dict__)
    zs.work()
    zs.find_dao()

执行结果:

d51815feaaebde1a76033e25950a9760.jpeg

从代码中,可以看到多继承的语法是相对简单的,通过多继承,子类可以同时获得多个父类的属性和方法,同时实现更大范围的代码复用。

但是,这个示例,只是简化了多继承的使用,逻辑也是相对简单的。接下来,我们稍微拧巴一下,探讨一下稍微复杂一点的情况:同名方法的覆盖与解析。

假如现在对打工人和自然人都添加一个吃饭的同名方法,代码稍微调整简化一下,这时候代码的执行会是什么结果呢?

# 定义打工人
class DaGongRen:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"打工人{self.name}吃老板画的饼")


# 定义自然人
class ZiRanRen:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"自然人{self.name}吃水果和蔬菜")


# 程序员同时是打工人和自然人
class Programmer(DaGongRen, ZiRanRen):
    def __init__(self, name):
        super().__init__(name)


if __name__ == '__main__':
    zs = Programmer('张三')
    zs.eat()

执行结果:

076527621e0cb266b5967bdcd38f7e24.jpeg

如果我们调整一下,继承中两个父类的顺序,重新执行一下:

7178f3e78f85fa75613b74d26c7c27b1.jpeg

可以看到,两个父类中的同名方法,跟定义多继承时的顺序有关。其实,真实的顺序或许更加复杂。

DFS、BFS或者其他

Python中支持多继承,且保证固定、一致、可预测的解析顺序,那么这是什么样一种顺序呢。稍微了解过《数据结构》的内容的同鞋,应该听说过两种树结果的遍历算法:DFS(Depth-First Search,深度优先)和BFS(Breadth-First Search,广度优先)。

Python中多继承中,方法的解析顺序会不会是DFS或者BFS中的一种呢。

我们简化一下代码,看下这样一个继承关系(不考虑object)

c248a36050277b02c1ac71e1a5685321.jpeg

A、C、D分别定义了同名方法,代码如下:

class A:
    def method(self):
        print("A method")


class B(A):
    pass


class C:
    def method(self):
        print("C method")


class D(C):
    def method(self):
        print("C method")


class E(B, D):
    pass


if __name__ == '__main__':
    e = E()
    e.method()

执行结果:

9b9a65b35288dbabcf237a1e4c920932.jpeg

从执行结果来看,实际调用了从A中继承的方法,所以,这种情况下,A、C、D都有method方法,但是实际调用了A的方法,而没有选择D的方法,看似采用了的DFS。

但是,如果我们换成这样一种继承关系呢:

9e0c9570dccb918d741ab7379ea3ef75.jpeg

这种继承关系,很像是一个菱形,就是大名鼎鼎的菱形继承/钻石继承。

A和C定义了同名方法,具体代码如下:

class A:
    def method(self):
        print("A method")


class B(A):
    pass


class C(A):
    def method(self):
        print("C method")


class D(B, C):
    pass


if __name__ == '__main__':
    d = D()
    d.method()

执行结果:

4f02ee8bdd4a58c3a72ebc3df96f8034.jpeg

从执行结果来看,实际调用了从C中继承的方法,所以,这种情况下,A、C都有method方法,但是实际调用了C的方法,而没有选择A的方法,看似又采用了的BFS。

其实,前面已经提到过,我们通过类的__mro__属性或者mro()方法,可以获得到方法的解析顺序,这个顺序在同一个继承关系中是固定、一致的。

而这个顺序既不是通过单纯的DFS生成,也不是通过BFS生成,而是基于C3线性化算法来实现的,这个算法保证如下三个原则:

1、一致性原则:一个类的MRO应该包含直接父类的MRO,并且保持他们的顺序。

2、本地优先原则:在MRO中,子类出现在父类的前面。

3、单调性原则:如果一个类的MRO包含另一个类的MRO,则该顺序在整个继承链中保持不变。

当然,这三个原则简单了解一下即可,感兴趣的可以细品……

总结

本文简单介绍了不同语言中多继承的设计实现方式,并通过代码介绍了Python中多继承中关于MRO顺序的设计实现,从而,在后续阅读到有关多继承的代码中,能够理解其中的解析实现方式及执行结果。

虽然Python支持多继承,但是使用多继承会导致代码的复杂性的增加,并且降低代码的可读性,所以,在实际使用中,非必要,应该尽量减少多继承的使用。

感谢您的拨冗阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值