前言

本章重点说明子类化内置类型的缺点,多重继承和方法解析顺序

子类化内置类型

python2.2之后,内置类型可以子类化了,但是内置类型(使用c语言编写)不会调用用户定义的类覆盖方法(即重写“Override”)

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

d = MyDict({'one':1, 'two':2})
print(d)
>>>{'one': 1, 'two': 2}

d['three'] = 3
print(d)
>>>{'one': 1, 'two': 2, 'three': [3, 3]}

d.update(four=4)
print(d)
>>>{'one': 1, 'two': 2, 'three': [3, 3], 'four': 4}

上述的例子的意思是,继承自内置类型的两个方法a()b()b()方法中调用了a(),我们重写了a(),此时b()仍然调用的是原来的a(), 而不是我们重写后的a()

原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。

这样的问题只发生在C语言实现的内置类型内部的方法委托上,解决这个问题的方法是,使用collections.UserDict, collections.UserList, collections.UserString

多重继承和方法解析顺序

 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
39
40
41
42
class A:
    def ping(self):
        print('ping: ', self)
    
class B(A):
    def pong(self):
        print('pong from B: ', self)

class C(A):
    def pong(self):
        print('pong from C: ', 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(self)

print(D.__mro__)
>>> (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

d = D()
d.pong()
>>> pong from B:  <__main__.D object at 0x7f13109ff438>

d.ping()
>>> ping:  <__main__.D object at 0x7fb439350438>
>>> post-ping:  <__main__.D object at 0x7fb439350438>

d.pingpong()
>>> ping:  <__main__.D object at 0x7f0890d65470>
>>> post-ping:  <__main__.D object at 0x7f0890d65470>
>>> ping:  <__main__.D object at 0x7f0890d65470>
>>> pong from B:  <__main__.D object at 0x7f0890d65470>
>>> pong from B:  <__main__.D object at 0x7f0890d65470>
>>> pong from C:  <__main__.D object at 0x7f0890d65470>
  • 可以看到d.pong()调用的是B.pong()
  • 注意ping()里的self
  • 注意d.pong()调用的是B.pong(), 方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序
  • self实例一致,(访问__mro__可以知道方法解析顺序)(Method Resolution Order,MRO)
  • 支持像c++一样, 使用类名加方法名访问, 注意必须显式传入self参数,因为这样访问的是未绑定方法(unbound method)。
  • 方法解析顺序使用C3算法计算。Michele Simionato的论文The Python 2.3 Method Resolution Order(https://www.python.org/download/releases/2.3/mro/)对Python方法解析顺序使用的 C3 算法做了权威论述

处理多重继承

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

使用多重继承时一定要明确一开始为什么创建子类,原因可能有:

  • 继承接口,创建子类型,实现“是什么”关系
  • 继承实现,通过重用避免代码重复

(2)使用抽象基类表示接口

  • 如果类的作用是定义接口,应该明确把它定义为抽象基类(即使用abc.ABC)

(3)通过混入Mixin重用代码

  • 如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么(is a)”关系,应该把那个类明确地定义为混入类(mixin class)
  • 混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。
  • 在名称中明确指明混入,例如MyClassMixin
  • 抽象基类可以作为混入

(4)不要子类化多个具体类

具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。例如,在下述代码中,如果Alpha是具体类,那么BetaGamma必须是抽象基类或混入:

1
2
class MyConcreteClass(Alpha, Beta, Gamma):
    pass

ps:这一点没有get到作者的点, 是不是有点太

(5)使用组合