类的多继承下的菱形问题

类的多继承下的菱形问题

一、类的分类

前面已经介绍过了,猛戳➦python之类的继承第三小节

二、继承原理

MRO介绍

对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表, python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的.

我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

  1. 子类会先于父类被检查
  2. 多个父类会根据它们在列表中的顺序被检查
  3. 如果对下一个类存在两个合法的选择,选择第一个父类

__mro__ 方法使用

  • 只有新式类才可以使用 (Python2中的类如果没继承object就无法使用)
  • [类名]__mro__ : 打印属性查找顺序, 是一个元组
  • [类名].mro() : 打印属性查找顺序, 是一个列表
# Python2 中
#coding:utf-8

class C(object):  # 继承object类,如果不继承,调用 mro 方法会报错 
    def run(self):
        print "run_C"

class B(C):
    pass

class A(B):
    pass

a = A()
a.run()  # run_C
print A.__mro__  
# (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <type 'object'>)

print A.mro()  
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <type 'object'>]

# Python3 中
class C:  # 已经自动继承了object类
    def run(self):
        print("run_C")

class B(C):
    pass

class A(B):
    pass

a = A()
a.run()  # run_C
print(A.__mro__)
# (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>)

print(A.mro())
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

总结:

类相关的属性查找(类名.属性,该类的对象.属性),都是参照该类的mro

ps:

1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去,

三、菱形继承问题介绍

多继承带来的菱形问题

前言:

大多数面向对象语言都不支持多继承, 而在python中是可以同时继承多个父类的.比如 A(B,C,D),这样固然可以带来一个子类可以对多个不同的父类加以重用的好处, 但也有可能引发著名的菱形问题(Diamond problem 或称钻石问题, 有时候也被称之为"死亡钻石"

java语言中,它不支持多继承,只支持继承一个父类

菱形问题介绍

什么是菱形问题?
  • 多个子类最终会继承到一个非object类中, 这种继承关系就会引发菱形问题

非菱形结构

  • 非菱形结构下, 经典类(python2)与新式类(python3)的属性查找顺序是一样的

  • 查找顺序: 经典类与新式类的属性查找顺序都是按照类括号中继承类的先后顺序依次按照一个分支一个分支的查找下去(注意: 每个分支的最终继承的非object类会作为当前分支结束的条件)

  • 如果继承关系为非菱形结构,则会按照先找 B 这一条分支,然后再找 C 这一条分支,最后找 D 这一条分支的顺序直到找到我们想要的属性

  • 非菱形结构 (两条查询路径最后分别继承了 F1 和 F4)

    img

class F1:
    def s1(self):
        print('F1:s1')
    def s2(self):
        print('F1:s2')

class F2(F1):
    # def s1(self):
    #     print('F2:s1')
    def s2(self):
        print('F2:s2')

class F4():
    def s1(self):
        print('F4:s1')
    def s2(self):
        print('F4:s2')

class F3(F2,F4):
    def s1(self):
        super().s1()  # 调用父类的s1方法,到底使用了哪个父类的s1
    def s2(self):
        print('F3:s2')


f1 = F3()
f1.s1()  # F1:s1

四、深度优先与广度优先

如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样

菱形结构

如果继承关系为菱形结构,那么经典类与新式类会有不同MRO,分别对应属性的两种查找方式:深度优先和广度优先

查找顺序

经典类深度优先, 新式类广度优先

  • 经典类:深度优先,会在检索第一条分支的时候就直接一条道走到黑,即会检索大脑袋(共同的父类)
  • 新式类:广度优先,会在检索第一条分支的时候检索大脑袋(共同的父类)
  • 深度优先与广度优先都是基于当前分支最终继承的非object类当作参考点. 如果是按照新式类则会在直接在第一分支下就找最终继承的非object类. 如果是广度优先则会在最后分支中才去找最终继承的非object类.
1、新式类----->广度优先
  • 不找多各类最后继承的同一个类,直接去找下一个父类

  • __mro__ : 只有新式类才有的属性, 可查看属性查找的顺序

    img

class G(object):
    # def test(self):
    #     print('from G')
    pass

class E(G):
    # def test(self):
    #     print('from E')
    pass

class B(E):
    # def test(self):
    #     print('from B')
    pass

class F(G):
    # def test(self):
    #     print('from F')
    pass

class C(F):
    # def test(self):
    #     print('from C')
    pass

class D(G):
    # def test(self):
    #     print('from D')
    pass

class A(B, C, D):
    def test(self):
        print('from A')

obj = A()
obj.test()
print(A.__mro__)
'''查找顺序
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class '__main__.G'>, <type 'object'>)
'''

2.经典类—>深度优先
  • 从左到右选路, 一条路走到黑

img

#coding:utf-8
# python2 解释器

class G():
    pass
    def test(self):
        print('G---test')

class F(G):
    pass
    # def test(self):
    #     print('FFF---test')
class E(G):
    pass
    # def test(self):
    #     print('EEE---test')
class D(G):
    pass
    # def test(self):
    #     print('DDD---test')
class B(E):
    pass
    # def test(self):
    #     print('BBB---test')
class C(F):
    pass
    # def test(self):
    #     print('CCC---test')

class A(B,C,D):
    pass
    # def test(self):
    #     print('AAA---test')

a=A()
a.test()  # G---test

四、总结:多继承到底要不要用?

要用,但是规避几点问题:

  1. 继承结构尽量不要过于复杂
  2. 推荐使用mixins机制, 这才是多继承环境下的正确打开方式, 核心思想就是在多继承的背景下满足继承的什么"是"什么的关系.
示例:
1.新式类
  • 菱形结构示例 (子类最后继承了同样的类, 都继承了 A1 和 A2), 可以看成两个菱形

4d1651e9189743b96c356a9fc9054ef

class A1:
    def foo(self):
        print('A1_foo')

class A2:
    def foo(self):
        print("A2_foo")

    def bar(self):
        print("A2_bar")

class B1(A1, A2):
    pass

class B2(A1, A2):
    def bar(self):
        print("B2_bar")

class C(B1, B2):
    pass

c = C()
c.foo()  # A1_foo
c.bar()  # B2_bar

print(C.__mro__)
'''输出
(<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A1'>, <class '__main__.A2'>, <class 'object'>)
'''

  • c.foo() 的查找顺序 : C ==> B1 ===> B2 ==> A1
  • c.bar() 的查找顺序 : C ==> B1 ==> B2
2.经典类

d415228a749e9175eaa2aea308686a7

#coding:utf-8
# python2.7 解释器
class A1():
    def foo(self):
        print 'A1_foo'

class A2():
    def foo(self):
        print "A2_foo"

    def bar(self):
        print "A2_bar"

class B1(A1, A2):
    pass

class B2(A1, A2):
    def bar(self):
        print "B2_bar"

class C(B1, B2):
    pass

c = C()
c.foo()  # A1_foo
c.bar()  # A2_bar
  • c.foo() 的查找顺序 : C ==> B1 ===> A1
  • c.bar() 的查找顺序 : C ==> B1 ==> A1 ===> A2

五、MixIns机制:本质还是基于多继承

MixIns机制核心介绍:

  • 就是为了能在多继承的背景下, 尽可能地提升多继承的可读性. python提供了一种基于类的命名方式, 它是通过定制一种命名规范来达到一种提示性的效果, 从而让多继承满足人的思维习惯. 明确什么"是"什么的关系. (提示: 变量的另一种规范 --> 常量)
  • 提示:它不是表达什么"是"什么的关系, 只是明确这是一个混合的关系, 而不是一个公共的父类, 而是混合的临时掺杂的类, 具有一定局限性.

MixIns机制的命名规范:

python对于mixin类的命名方式一般以 MixIn, able, ible 为后缀. 不过推荐还是使用MixIn作为标识, 寓意更加明确.

MixIns机制所带来的的好处:

因为多继承带来的缺点, 使用MixIns机制可以变相的优化程序的可读性和可维护性以及可扩展性.

class Vehicle:  # Vehicle 交通工具
    pass

class FlyableMixIn:  # Flyable 飞行器
    def fly(self):
        pass

# 表达什么"是"什么的关系的类"Vehicle", 一定要放在结尾
class CivilAircraft(FlyableMixIn, Vehicle):  # CivilAircraft 民航飞机
    pass

class Helicopter(FlyableMixIn, Vehicle):  # Helicopter 直升飞机
    pass

class Car(Vehicle):  # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
    pass

# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路

#可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能,但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。

使用Mixin类实现多重继承要非常小心

  • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
  • 其次它必须责任单一,如果有多个Mixin,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
  • 然后,它不依赖于子类的实现,是个独立的个体
  • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)

​ Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向,如下

class Displayer:
    def display(self, message):
        print(message)


class LoggerMixin:
    def log(self, message, filename='logfile.txt'):
        with open(filename, 'a') as fh:
            fh.write(message)

    def display(self, message):
        super().display(message) # super的用法请参考下一小节
        self.log(message)


class MySubClass(LoggerMixin, Displayer):
    def log(self, message):
        super().log(message, filename='subclasslog.txt') 


obj = MySubClass()
obj.display("This string will be shown and logged in subclasslog.txt")


# 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性
#[<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]

# 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码
# 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"
# 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

贾维斯Echo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值