《流畅的Python》读书笔记14(第十二章:继承的优缺点)

233 阅读4分钟

本章探讨继承和子类化,重点是说明对Python而言尤为重要的两个细节:

  • 子类化内置类型的缺点
  • 多重继承和方法解析顺序

12.1 子类化内置类型很麻烦

内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。

>>> class DoppelDict(dict):

...     def __setitem__(self, key, value):

...         super().__setitem__(key, [value]*2)

... 

>>> dd = DoppelDict(one=1)

>>> dd

{'one': 1}

>>> dd['two'] = 2

>>> dd

{'one': 1, 'two': [2, 2]}

>>> dd.update(three=3)

>>> dd

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

原生类型的这种行为违背了面向对象变成的一个基本原则:始终应该从实例所属的类开始搜索方法。

不只实例内部的调用有这个问题,内置类型的方法调用的其他类的方法,如果被覆盖了,也不会被调用。

示例12-2 dict.update方法会忽略AnswerDict.__getitem__方法

>>> class AnswerDict(dict):

...     def __getitem__(self, key):

...         return 42

... 

>>> ad = AnswerDict(a='foo')

>>> ad['a']

42

>>> d = {}

>>> d.update(ad)

>>> d['a']

'foo'

>>> d

{'a': 'foo'}

如果不实例化dict,而是子类化collections.UserDict。就不会有问题

示例12-3

>>> import collections

>>> class DoppelDict2(collections.UserDict):

...     def __setitem__(self, key, value):

...         super().__setitem__(key, [value]*2)

... 

>>> dd = DoppelDict2(one=1)

>>> dd

{'one': [1, 1]}

>>> dd['two'] = 2

>>> dd

{'one': [1, 1], 'two': [2, 2]}

>>> dd.update(three = 3)

>>> dd

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

>>> class AnswerDict2(collections.UserDict):

...     def __getitem__(self, key):

...         return 42

... 

>>> ad = AnswerDict2(a = 'foo')

>>> ad['a']

42

>>> d = {}

>>> d.update(ad)

>>> d['a']

42

>>> d

{'a': 42}

与继承,尤其是多重继承有关的另一个问题是:如果同级别的超类定义了同名属性,Python如果确定使用哪一个?

12.2 多重继承和方法解析顺序

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

image.png 示例12-4 diamond.py:上图中的A、B、C、和D四个类

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(self)

Python能却分d.pong()调用的是哪个方法,是因为Python会安装特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order, MRO)。类都有一个名为__mro__的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object类。

D.__mro__
(<class 'diamond.D'>, <class 'diamond.B'>, 
<class 'python_cook.diamond.C'>, 
<class 'diamond.A'>, <class 'object'>)

示例12-7 pingpong方法的5个调用

d = D()
d.pingpong()
ping: <diamond.D object at 0x7f8d7069acc0>
post-ping <diamond.D object at 0x7f8d7069acc0>
ping: <diamond.D object at 0x7f8d7069acc0>
pong: <diamond.D object at 0x7f8d7069acc0>
pong: <diamond.D object at 0x7f8d7069acc0>
PONG: <diamond.D object at 0x7f8d7069acc0>

示例12-8 查看几个类的__mro__属性

>>> bool.__mro__

(<class 'bool'>, <class 'int'>, <class 'object'>)

>>> def print_mro(cls):

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

... 

>>> print_mro(bool)

bool,int,object

>>> import numbers

>>> print_mro(numbers.Integral)

Integral,Rational,Real,Complex,Number,object

>>> import io

>>> print_mro(io.BytesIO)

BytesIO,_BufferedIOBase,_IOBase,object

>>> print_mro(io.TextIOWrapper)

TextIOWrapper,_TextIOBase,_IOBase,object

Tkinter复杂的多重继承图

>>> import tkinter

>>> print_mro(tkinter.Text)

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

>>>

image.png

12.3 多重继承的真实应用

image.png Tkinter GUI类层次结构的UML简图

来看图中的几个类:

  1. Toplevel:表示Tkinter应用程序的顶层窗口的类
  2. Widget: 窗口中所有可见对象的超类
  3. Button: 普通的按钮小组件
  4. Entry: 单行可编辑文本字段
  5. Text:多行可编辑文本字段
>>> import tkinter

>>> print_mro(tkinter.Toplevel)

Toplevel,BaseWidget,Misc,Wm,object

>>> print_mro(tkinter.Widget)

Widget,BaseWidget,Misc,Pack,Place,Grid,object

>>> print_mro(tkinter.Button)

Button,Widget,BaseWidget,Misc,Pack,Place,Grid,object

>>> print_mro(tkinter.Entry)

Entry,Widget,BaseWidget,Misc,Pack,Place,Grid,XView,object

>>> print_mro(tkinter.Text)

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

12.4 处理多重继承

下面是避免把类推搅乱的一些建议。

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

  2. 使用抽象基类显式表示接口

  3. 通过混入重用代码 如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class).

  4. 在名称中明确指明混入

  5. 抽象基类可以作为混入,反过来则不成立

  6. 不要子类化多个具体类

  7. 为用户提供聚合类 8.“优先使用对象组合,而不是类继承”

12.5 一个现代示例:Django通用视图中的混入

在Django中,视图是可调用的对象,它的参数是表示HTTP请求的对象,返回值是一个表示HTTP相应的对象。我们要关注的是这些响应对象。响应可以说简单的重定向,也可以说复杂的内容。

image.png