2
导语:本文章记录了本人在学习Python基础之面向对象篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。

本文重点:

1、不要试图在内置类型的子类中重写方法,可以继承collections的可拓展类寻求变通;
2、掌握多重继承中的MRO和Super;
3、了解处理多重继承的一些建议。

一、子类化内置类型的缺点

1、内置类型的方法不会调用子类覆盖的方法

内置类可以子类化,但是内置类型的方法不会调用子类覆盖的方法。下面以继承dict的自定义子类重写__setitem__为例说明:

class ModifiedDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key,[value]*2)
a=ModifiedDict(one=1)
a["two"]=2
print(a)
a.update(three=3)
print(a)#输出{'one': 1, 'two': [2, 2], 'three': 3}

从输出可以看到,键值对one=1和three=3存入a时均调用了dict的__setitem__,只有[]运算符会调用我们预先覆盖的方法。
问题的解决方式在于不去子类化dict,而是子类化colections.UserDict。

2、子类化collections中的类

用户自定义的类应该继承collections模块,如UserDict,UserList,UserString。这些类做了特殊设计,因此易于拓展。子类化UserDict的代码如下:

from collections import UserDict
class ModifiedDict(UserDict):
    def __setitem__(self, key, value):
        super().__setitem__(key,[value]*2)
b=ModifiedDict(one=1)
b["two"]=2
b.update(three=3)
print(b)#输出{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}

小结:上述问题只发生在C语言实现的内置类型子类化情况中,而且只影响直接继承内置类型的自定义类。相反,子类化使用Python编写的类,如UserDict或MutableMapping就不会有此问题。

二、多重继承

1、方法解析顺序(Method Resolution Order,MRO)

在多重继承中存在不相关的祖先类实现同名方法引起的冲突问题,这种问题称作“菱形问题”。Python依靠特定的顺序遍历继承图,这个顺序叫做方法解析顺序。如图,左图是类的UML图,右图中的虚线箭头是方法解析顺序。
图片描述

2、super

提到类的属性__mro__,就会提到super:

super 是个类,既不是关键字也不是函数等其他数据结构。

作用:super是子类用来调用父类方法的。
语法:super(a_type, obj);
a_type是obj的__mro__,当然也可以是__mro__的一部分,同时issubclass(obj,a_type)==true

举个例子, 有个 MRO: [A, B, C, D, E, object]
我们这样调用:super(C, A).foo()
super 只会从 C 之后查找,即: 只会在 D 或 E 或 object 中查找 foo 方法。

下面构造一个菱形问题的多重继承来深化理解:

class A:
    def ping(self):
        print("A-ping:",self)
class B(A):
    def pong(self):
        print("B-pong:",self)
class C(A):
    def pong(self):
        print("C-PONG:",self)
class D(B, C):
    def ping(self):
        print("D-ping:",self)
        super().ping()
    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super(B,D).pong(self)
d=D()
d.pingpong()
print(D.mro())

输出如下:

D-ping: <__main__.D object at 0x000001B77096EAC8>
A-ping: <__main__.D object at 0x000001B77096EAC8>#前两行对应self.ping()。
A-ping: <__main__.D object at 0x000001B77096EAC8>#此处super调用父类的ping方法。
B-pong: <__main__.D object at 0x000001B77096EAC8>
C-PONG: <__main__.D object at 0x000001B77096EAC8>#此处从B之后搜索父类的pong()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]#类D的__mro__,数据以元组的形式存储。

分析:d.pingpong()执行super.ping(),super按照MRO查找父类的ping方法,查询在类B到ping之后输出了B.ping()。

3、处理多重继承的建议

(1)把接口继承和实现继承区分开;

  • 继承接口:创建子类型,是框架的支柱;
  • 继承实现:通过重用避免代码重复,通常可以换用组合和委托模式。

(2)使用抽象基类显式表示接口;
(3)通过混入重用代码;
混入类为多个不相关的子类提供方法实现,便于重用,但不会实例化。并且具体类不能只继承混入类。
(4)在名称中明确指明混入;
Python中没有把类声明为混入的正规方式,Luciano推荐在名称中加入Mixin后缀。如Tkinter中的XView应变成XViewMixin。
(5)抽象基类可以作为混入,反过来则不成立;
抽象基类与混入的异同:

  • 抽象基类会定义类型,混入做不到;
  • 抽象基类可以作为其他类的唯一基类,混入做不到;
  • 抽象基类实现的具体方法只能与抽象基类及其超类中的方法协作,混入没有这个局限。

(6)不要子类化多个具体类;
具体类可以没有,或者至多一个具体超类。
例如,Class Dish(China,Japan,Tofu)中,如果Tofu是具体类,那么China和Japan必须是抽象基类或混入。
(7)为用户提供聚合类;
聚合类是指一个类的结构主要继承自混入,自身没有添加结构或行为。Tkinter采纳了此条建议。
(8)优先使用对象组合,而不是类继承。
优先使用组合可以令设计更灵活。
组合和委托可以代替混入,但不能取代接口继承去定义类型层次结构。

注:super调用知识引自
作者: mozillazg
链接:https://segmentfault.com/a/11...


Hanwencheng
163 声望5 粉丝

Be yourself.