深入理解Python之继承

继承内置类型的技巧

Python2.2之前,不能够继承内置类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)


dd = DoppelDict(one=1)
print(dd)

dd['two'] = 2
print(dd)
dd.update(three=3)
print(dd)

result:
{'one': 1}
{'one': 1, 'two': [2, 2]}
{'one': 1, 'two': [2, 2], 'three': 3}

DoppelDict当排序时,复制值.__init__忽略__setitem__被重写,因此one没有被复制。[]操作符调用__setitem__,因此two被复制。update方法没有使用重写的__setitem__方法。因此three没有复制

方法的寻找总是从目标对象开始,然后向上寻找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AnswerDict(dict):
def __getitem__(self, key):
return 42

ad = AnswerDict(a='foo')
print(ad['a'])

d = {}
d.update(ad)
print(d['a'])
print(d)

result:
42
foo
{'a': 'foo'}

AnswerDict.__getitem__()总是返回42.d是个dict,使用ad更新它,忽略了AnswerDict.__getitem__()

直接继承内置类型如dict或者list容易出错,因为内置的方法大多数会忽略用户自定的方法。因此从collections模块中使用UserDict,UserLisat,UserString来派生类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DoppelDict2(collections.UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)


dd = DoppelDict2(one=1)
print(dd)
dd['two'] = 2
print(dd)
dd.update(three=3)
print(dd)

result:
{'one': [1, 1]}
{'one': [1, 1], 'two': [2, 2]}
{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}

多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

class A:
def ping(self):
print('ping:', self)


class B(A):
def pong(self):
print('pong:', self)


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


class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)

def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong()

d = D()
print(d.pong())
print(C.pong(d))

print(D.__mro__)

result:
pong: <__main__.D object at 0x1024a66d8>
PONG: <__main__.D object at 0x1024a66d8>
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

当在一个类上直接调用方法时,需要显式传入self。调用方法的顺序依据MRO(__mro__)采用C3算法

1
2
3
4
5
6
7
8

def print_mro(cls):
print(', '.join(c.__name__ for c in cls.__mro__))

print_mro(tkinter.Text)

result:
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

其搜索路径就是按着Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object从左往右搜索

对于多重继承来说,继承顺序决定了搜索顺序

真实的多重继承案例

适配器模式就使用了多重继承.Python中最常见的多重继承是collection.abc包。标准库Tkinter GUI使用了大量的多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
print_mro(tkinter.Toplevel)
print_mro(tkinter.Widget)
print_mro(tkinter.Button)
print_mro(tkinter.Entry)
print_mro(tkinter.Text)

result:
Toplevel, BaseWidget, Misc, Wm, object
Widget, BaseWidget, Misc, Pack, Place, Grid, object
Button, Widget, BaseWidget, Misc, Pack, Place, Grid, object
Entry, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, object
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

应对多重继承

  1. 区分接口继承和实现继承

    • 接口继承创建子类型,实现is-a关系
    • 实现继承利于代码复用
  2. 使用ABCs来创造接口
    

  3. 使用Mixin来使代码复用

    如果一个类被设计成为多个不相关的类提供复用,那么请继承Mixin, Mixin不会定义新类型,只是封装方法以便复用.Mixin不该被实例化,任何实体类都不该仅仅只继承Mixin. 每个Mixin应该只提供一种单一特定的行为,实现几个紧密相关的方法

  4. 显式命名Mixin

    没有标准的方式来说明一个类是Mixin,因此建议使用Mixin作为后缀

  5. ABC可能是Mixin,反过来不一定成立

  6. 不要继承多个实体类

  7. 提供集合类给用户

  8. 组合胜过继承