资讯专栏INFORMATION COLUMN

Python学习之路31-继承的利弊

tinylcy / 347人阅读

摘要:使用抽象基类显示表示接口如果类的作用是定义接口,应该将其明确定义为抽象基类。此外,抽象基类可以作为其他类的唯一基类,混入类则决不能作为唯一的基类,除非这个混入类继承了另一个更具体的混入这种做法非常少见。

《流畅的Python》笔记

本篇是“面向对象惯用方法”的第五篇,我们将继续讨论继承,重点说明两个方面:继承内置类型时的问题以及多重继承。概念比较多,较为枯燥。

1. 继承内置类型

内置类型(C语言编写)的方法通常会忽略用户重写的方法,这种行为体现在两方面:

内置类型A的子类ChildA即使重写了A中的方法,当ChildA调用这些方法时,也不一定调用的就是重写的版本,而依然可能调用A中的版本;

内置类型B调用ChildA的方法时,调用的也不一定是被ChildA重写的方法,可能依然会调用A的版本。

dict__getitem__方法为例,即使这个方法被子类重写了,内置类型的get()方法也不一定调用重写的版本:

# 代码1.1
>>> class MyDict(dict):
...     def __getitem__(self, key):
...         return "Test"   # 不管要获取谁,都返回"Test"
...    
>>> child = MyDict({"one":1, "two":2})
>>> child
{"one": 1, "two": 2}    # 正常
>>> child["one"]
"Test"    # 此时也是正常的
>>> child.get("one")
1   # 这里就不正常了,按理说应该返回"Test"
>>> b = {}
>>> b.update(child)
>>> b  # 并没有调用child的__getitem__方法
{"one": 1, "two": 2}

这是在CPython中的情况,这些行为其实违背了面向对象编程的一个基本原则,即应该始终从实例所属的类开始搜索方法,即使在超类实现的类中调用也应该如此。但实际是可能直接调用基类的方法,而不先搜索子类。这种设定并不能说是错误的,这只是一种取舍,毕竟这也是CPython中的内置类型运行得快的原因之一,但这种方式就给我们出了难题。这种问题的解决方法有两个:

重写从内置类型继承来的所有方法(要真这样,那我还继承干啥?),或者查看源码,把相关的方法都给重写了(谁的记性能这么好?);

第二种方法才是推荐的方法:如果要继承内置类型,请从collections模块中继承,比如继承自UserListUserDictUserString。这些类不是用C语言写的,而是用纯Python写的,并且严格遵循了上述面向对象的原则。如果上述代码中的MyDict继承自UserDict,行为则会合乎预期。

强调:本节所述问题只发生在C语言实现的内置类型内部的方法委托上,而且只影响直接继承内置类型的自定义类。如果子类继承自纯Python编写的类,则不会有此问题。

2.多重继承

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的超类实现同名方法引起。这种冲突称为”菱形冲突“。

2.1 多重继承的示例

下面是我们要实现的类的UML图:

红线表示超类的调用顺序,以下是它的实现:

# 代码2.1
class A:
    def ping(self):
        print("ping in A:", self)

class B(A):
    def pong(self):
        print("pong in B:", self)

class C(A):
    def pong(self):
        print("PONG in C:", self)

class D(B, C):
    def ping(self):
        super().ping()
        print("ping in D:", self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)   # 在定义时调用特定父类的写法,显示传入self参数

# 下面是它在控制台中的调用情况
>>> from diamond import *
>>> d = D()
>>> d.pong()
pong in B: 
>>> d.pingpong()
ping in A:    # self.ping()
ping in D: 
ping in A:    # super().ping()
pong in B:    # self.pong()
pong in B:    # super().pong()
PONG in C:    # C.pong(self)
>>> C.pong(d)    # 在运行时调用特定父类的写法,显示传入实例参数
PONG in C: 
>>> D.__mro__   # Method Resolutino Order,方法解析顺序,上一篇文章中有所提及
(, , , 
 , )

类都有一个名为__mro__ 的属性,它的值是一个元组,按一定顺序列举超类,这个顺序由C3算法计算。

方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。例如,如果D类的声明改为class D(C, B),那么D则会先搜索C,再搜索B

若想把方法调用委托给超类,推荐的做法是使用内置的super()函数;同时,还请注意上述调用特定超类的语法。然而,使用super()是最安全的,也不易过时。调用框架或不受自己控制的类层次结构中的方法时,尤其应该使用super()

2.2 处理多重继承的建议

继承有很多用途,而多重继承增加了可选方案和复杂度。使用多重继承容易得出令人费解和脆弱的设计。以下是8条避免产生混乱类图的建议:

把接口继承和实现继承区分开

在使用多重继承时,一定要明白自己为什么要创建子类:

继承接口,创建子类,实现“是什么(”is-a”)”关系;

继承实现,通过重用避免代码重复

其实这俩经常同时出现,不过只要有可能,一定要明确这么做的意图。通过继承重用代码是实现细节,通常可以换成用组合和委托的模式,而接口继承则是框架的支柱。

使用抽象基类显示表示接口

如果类的作用是定义接口,应该将其明确定义为抽象基类。

通过“混入类”实现代码重用

如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“is-a”关系,则应该把那个类明确定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。

在名称中明确指明混入

由于Python没有把类明确声明为混入的正式方式,实际的做法是在类名后面加入Mixin后缀。Python的GUI库Tkinter没有采用这种方法,这也是它的类图十分混乱的原因之一,而Django则采用了这种方式。

抽象基类可以作为混入类,但混入类不能作为抽象基类

抽象基类可以实现具体方法,因此可以作为混入类使用。但抽象基类能定义数据类型,混入类则做不到。此外,抽象基类可以作为其他类的唯一基类,混入类则决不能作为唯一的基类,除非这个混入类继承了另一个更具体的混入(这种做法非常少见)。

但值得注意的是,抽象基类中的具体方法只是一种便利措施,因为它只能调用抽象基类及其超类中定义了的方法,那么用户自行调用这些方法也可以实现同样的功能,所以,抽象基类也并不常作为混入类。

不要从多个具体类继承

应该尽量保证具体类没有或者最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类外,其余的都应该是抽象基类或混入类。

为用户提供聚合类

如果抽象基类或混入类的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们结合起来,这种类被称为聚合类。比如tkinter.Widget类,它的定义如下:

# 代码2.2
class Widget(BaseWidget, Pack, Place, Grid):  # 省略掉了文档注释
    pass

它的定义体是空的,但通过这一个类,提供了四个超类的全部方法。

优先使用对象组合,而不是类继承

优先使用组合能让设计更灵活。即便是单继承,这个原则也能提升灵活性,因为继承是一种紧耦合,而且较高的继承树容易倒。组合和委托还可以代替混入类,把行为提供给不同的类,但它不能取代接口继承,因为接口继承定义的是类层次结构。

迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41945.html

相关文章

  • Python学习之路8.2-对Python补充

    摘要:本章主要是对上一章类的补充。对于多态的补充子类可以被看成是父类的类型,但父类不能被看成是子类的类型。仍然以类为例,动物里有哺乳动物,卵生动物,有能飞的动物和不能飞的动物,这是两种大的分类方式。一般在中,以为结尾类的都作为接口。 《Python编程:从入门到实践》笔记。本章主要是对上一章Python类的补充。 1. 从一个类派生出所有类 上一篇文章说道Python类的定义与继承一般是如下...

    liukai90 评论0 收藏0
  • Vue:scoped与module使用与利弊

    摘要:一个应用是离不开与,其中充斥的整个项目中。下面我会分别对与解决方案进行说明,最后在分析它们的利弊与选择。不过一个子组件的根节点会同时受其父组件有作用域的和子组件有作用域的的影响。但它的局限性是适用于中小项目中。 showImg(https://segmentfault.com/img/bVbnIPd?w=900&h=383); 一个web应用是离不开html、css与js,其中css充...

    zhoutk 评论0 收藏0
  • Vue:scoped与module使用与利弊

    摘要:一个应用是离不开与,其中充斥的整个项目中。下面我会分别对与解决方案进行说明,最后在分析它们的利弊与选择。不过一个子组件的根节点会同时受其父组件有作用域的和子组件有作用域的的影响。但它的局限性是适用于中小项目中。 showImg(https://segmentfault.com/img/bVbnIPd?w=900&h=383); 一个web应用是离不开html、css与js,其中css充...

    zr_hebo 评论0 收藏0
  • Vue:scoped与module使用与利弊

    摘要:一个应用是离不开与,其中充斥的整个项目中。下面我会分别对与解决方案进行说明,最后在分析它们的利弊与选择。不过一个子组件的根节点会同时受其父组件有作用域的和子组件有作用域的的影响。但它的局限性是适用于中小项目中。 showImg(https://segmentfault.com/img/bVbnIPd?w=900&h=383); 一个web应用是离不开html、css与js,其中css充...

    missonce 评论0 收藏0
  • Python学习之路30-接口:从协议到抽象基类

    摘要:本篇内容将从鸭子类型的动态协议,逐渐过渡到使接口更明确能验证实现是否符合规定的抽象基类。抽象基类介绍完动态实现接口后,现在开始讨论抽象基类,它属于静态显示地实现接口。标准库中的抽象基类从开始,标准库提供了抽象基类。 《流畅的Python》笔记。本篇是面向对象惯用方法的第四篇,主要讨论接口。本篇内容将从鸭子类型的动态协议,逐渐过渡到使接口更明确、能验证实现是否符合规定的抽象基类(Abst...

    LucasTwilight 评论0 收藏0

发表评论

0条评论

tinylcy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<