[...] 我们需要一个更好的关于继承的理论(现在仍然如此)。例如,继承和实例化(这是一种继承)混淆了语用学(例如分解代码以节省空间)和语义学(用于很多任务,例如:专业化、泛化、规范化等)。

--Alan Kay, The Early History of Smalltalk

本章是关于继承和子类化。我将假设您对这些概念有基本的了解,您可能通过阅读 Python 教程或使用其他主流面向对象语言(如 Java、C# 或 C++)的经验而了解这些概念。本章我们将重点介绍 Python 的四个特性:

  • super() 函数。
  • 子类化内置类型的陷阱。
  • 多重继承和方法解析顺序。
  • mixin(混入)类

多重继承是一个类同时拥有多个基类的能力。C++ 支持这个特性; Java 和 C# 则不支持。许多人认为多重继承是得不偿失的。在早期 C++ 代码库中被认为滥用之后,这也可能坚定了Java摒除多重继承的决心。

本章为从未使用过的人介绍了多重继承,并提供了一些指导,在必须使用继承时如何应对单继承或多重继承。

到 2021 年,由于超类和子类之间的紧耦合,因此普遍反对过度使用继承——不仅仅是多重继承。紧耦合意味着对程序某一部分的更改可能会对其他部分产生意想不到且很深的影响,使系统变得脆弱且难以理解。

然而,我们必须维护设计有复杂类层次结构的现有系统,或者使用强制我们使用继承的框架——有时甚至是多重继承。

我将通过标准库、Django Web 框架和 Tkinter GUI 工具包来说明多重继承的实际应用。

本章的新内容

没有与本章主题相关的 Python 新功能,但我根据第二版技术审阅者的反馈对其进行了大量编辑,尤其是 Leonardo Rochael 和 Caleb Hattingh。

我写了一个新的开头部分,重点介绍了 super() 内置函数,并更改了“多重继承和方法解析顺序”中的示例,以更深入地探索 super() 如何支持协作多重继承。

“Mixin类型”也是新的,而“现实世界中的多重继承”经过重组,涵盖了标准库中更简单的混合示例,在复杂的 Django 和复杂的 Tkinter 层次结构之前。

正如章节标题所暗示的,继承的注意事项一直是本章的主题之一。但是越来越多的开发人员认为继承本身就是问题,以至于我在“章节总结”和“进一步阅读”的末尾添加了几段关于避免继承的段落。

我们将从神秘的 super() 函数的概述开始。

super() 函数

使用 super() 内置函数并保持一致性对于可维护的面向对象的 Python 程序至关重要。

当子类覆盖超类的方法时,覆盖的方法通常需要调用超类的对应方法。以下是推荐的方法,来自 collections 模块文档中的示例, OrderedDict Examples and Recipes:

class LastUpdatedOrderedDict(OrderedDict):"""Store items in the order they were last updated"""def __setitem__(self, key, value):super().__setitem__(key, value)self.move_to_end(key)

为了实现这个方法,LastUpdatedOrderedDict 将 __setitem__ 覆盖为:

  1. 使用 super().__setitem__ 调用在超类上的该方法,让它插入或更新键/值对。
  2. 调用 self.move_to_end 以确保更新的键位于最后一个位置。

调用重写的 __init__ 方法以允许超类在实例中进行初始化尤为重要。

TIP:

如果您在 Java 中学习了面向对象编程,您可能还记得 Java 构造函数方法会自动调用超类的无参数构造函数。 Python 不会这样做。你必须习惯按照这个模式编写__init__:

    def __init__(self, a, b) :super().__init__(a, b)...  # more initialization code

你也许见过不使用 super() 而是直接在超类上调用方法的代码,如下所示:

class NotRecommended(OrderedDict):"""This is a counter example!"""def __setitem__(self, key, value):OrderedDict.__setitem__(self, key, value)self.move_to_end(key)

这种替代方法同样适用于这种特殊情况,但由于两个原因不推荐使用。

首先,它对基类进行了硬编码。名称 OrderedDict 出现在 class 语句和 __setitem__ 中。如果将来有人更改 class 语句更改了基类或添加另一个基类,他们可能会忘记更新 __setitem__ 的主体,从而引入缺陷。

第二个原因是 super 实现了处理具有多重继承的类层次结构的逻辑。我们将在 “Multiple Inheritance and Method Resolution Order”中回到这部分。总结一下关于 super 的复习,回顾一下我们如何在 Python 2 中调用它,因为带有两个参数的旧签名揭示了:

class LastUpdatedOrderedDict(OrderedDict):"""This code works in Python 2 and Python 3"""def __setitem__(self, key, value):super(LastUpdatedOrderedDict, self).__setitem__(key, value)self.move_to_end(key)

super 的两个参数现在都是可选的。当在方法中调用 super() 时,Python 3 字节码编译器通过检查周围的上下文进行自动提供。这些参数是:

type:

实现所需方法的超类的搜索路径的开始。默认情况下,它是调用 super() 方法的类。

object_or_type:

作为方法调用接收者的对象(例如实例方法调用)或类(对于类方法调用)。默认情况下,如果 在实例中调用了super(),则这个参数的值是 self 。

无论是您还是编译器提供这些参数,super() 调用都会返回一个动态代理对象,该对象在type参数对应的超类中查找方法(例如示例中的 __setitem__),并将这个动态代理对象绑定到 object_or_type,这样我们就不需要显式传递接收者(self)然后调用该方法。

在 Python 3 中,您仍然可以显式向 super() 传递第一个和第二个参数。但是只有在特殊情况下才需要这样做,例如跳过部分 MRO 以进行测试、调试或解决超类中的异常行为。

现在让我们讨论对内置类型进行子类化时的注意事项。

子类化内置类型很麻烦

在 Python 的最早版本中,不能子类化内置类型(如 list 或 dict)。从 Python 2.2 开始,这样做是可能的,但有一个重要的注意事项:内置类型的代码(用 C 编写)通常不会调用被用户定义的类覆盖的方法。

在 PyPy 的文档中,关于PyPy 和 CPython 之间的差异,Subclasses of built-in types部分中对该问题进行了正确的简短描述:

在官方途径中中,关于何时隐式调用内置类型的子类覆盖的方法,CPython 根本没有制定规则。基本上,这些方法永远不会被同一对象的其他内置方法调用。例如, dict 子类中被覆盖的 __getitem__() 方法不会被内置类型的 get() 方法调用。

示例 14-1 说明了这个问题。

例 14-1。我们覆盖的__setitem__ 方法被内置 dict 的 __init__ 和 __update__ 方法忽略

>>> class DoppelDict(dict):
...     def __setitem__(self, key, value):
...         super().__setitem__(key, [value] * 2)  1
...
>>> dd = DoppelDict(one=1)  2
>>> dd
{'one': 1}
>>> dd['two'] = 2  3
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3)  4
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}
  1. DoppelDict.__setitem__ 存储时会重复存储一个包含两个值的列表(,只是为了有一个可见的效果)。它把职责委托给超类。
  2. 从 dict 继承的 __init__ 方法显然忽略了 __setitem__ 被覆盖:'one' 的值没有重复。
  3. [] 运算符调用了我们覆盖的 __setitem__ 并按预期工作:'two' 映射到重复值 [2, 2]。
  4. dict 中的updata方法也没有使用我们的 __setitem__ 版本:'three' 的值没有重复。

内置类型的这种行为违反了面向对象编程的基本规则:搜索方法应该始终从接收者的类(self)开始,即使调用发生在超类中实现的方法内部。这就是所谓的“后期绑定”,Smalltalk 的知名人士 Alan Kay 认为这是面向对象编程的一个关键特性:在任何形式的 x.method() 的调用中,要调用的方法必须在运行时确定,基于接收者的类 x是什么。这样的设计导致了我们在“标准库中 __missing__ 的使用不一致”中看到的问题。

问题不仅限于实例内的调用——self.get() 是否调用 self.__getitem__(),也会发生在内置方法调用其他类的重写方法。

示例 14-2 改编自 PyPy documentation。

例 14-2。 dict.update 绕过了 AnswerDict 的 __getitem__

>>> class AnswerDict(dict):
...     def __getitem__(self, key):  1
...         return 42
...
>>> ad = AnswerDict(a='foo')  2
>>> ad['a']  3
42
>>> d = {}
>>> d.update(ad)  4
>>> d['a']  5
'foo'
>>> d
{'a': 'foo'}
  1. 不管传入什么键,AnswerDict.__getitem__ 总是返回 42。
  2. ad 是一个以键值对 ('a', 'foo') 进行初始化的 AnswerDict。
  3. ad['a'] 返回 42,正如预期的那样
  4. d 是一个普通 dict 的实例,我们使用用 ad 更新d。
  5. dict.update 方法忽略了我们的 AnswerDict.__getitem__。

Warning:

直接对 dict 或 list 或 str 等内置类型进行子类化容易出错,因为内置类型的方法大多会忽略用户定义的覆盖的方法。不要子类化内置类型,而是继承 UserDict、UserList 和 UserString 等collections模块派生类,它们做了特殊设计,易于扩展。

如果子类化一个 collections.UserDict 而不是 dict,则示例 14-1 和 14-2 中暴露的问题都已修复。请参见示例 14-3。

例 14-3。 DoppelDict2 和 AnswerDict2 按预期工作,因为它们扩展 UserDict 而不是 dict

>>> 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
{'two': [2, 2], 'one': [1, 1]}
>>> dd.update(three=3)
>>> dd
{'two': [2, 2], 'three': [3, 3], 'one': [1, 1]}
>>>
>>> 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}

作为测量对内置函数进行子类化所需的额外工作的实验,我将示例 3-9 中的 StrKeyDict 类重写为继承 dict 而不是 UserDict。为了使其通过相同的测试,我必须实现 __init__、get 和 update方法,因为从 dict 继承的版本拒绝与覆盖的 __missing__、__contains__ 和 __setitem__ 合作。Example 3-9 中的 UserDict 子类有 16 行,而实验 dict 子类最终有 33 行。

需要明确的是:本节涵盖的问题仅适用于内置类型的 C 语言代码中的方法委托,并且仅影响直接从这些类型派生的类。如果你子类化一个用 Python 编码的基类,比如 UserDict 或 MutableMapping,你就不会被这个问题困扰。

现在让我们关注一个多重继承带来的问题:如果一个类有两个超类,当我们调用 super().attr 时,如果两个超类都有一个同名的属性,Python 如何决定使用哪个属性?

多重继承和方法解析顺序

图 14-1。左图:leaf1.ping() 调用的方法解析顺序。右图:leaf1.pong() 调用的方法解析顺序。

当超类实现同名方法时,任何实现多重继承的语言都需要处理潜在的命名冲突。这称为“钻石问题”,如图 14-1 和示例 14-4 所示。

class Root:  1def ping(self):print(f'{self}.ping() in Root')def pong(self):print(f'{self}.pong() in Root')def __repr__(self):cls_name = type(self).__name__return f'<instance of {cls_name}>'class A(Root):  2def ping(self):print(f'{self}.ping() in A')super().ping()def pong(self):print(f'{self}.pong() in A')super().pong()class B(Root):  3def ping(self):print(f'{self}.ping() in B')super().ping()def pong(self):print(f'{self}.pong() in B')class Leaf(A, B):  4def ping(self):print(f'{self}.ping() in Leaf')super().ping()
  1. Root 提供 ping、pong 和 __repr__ 以使输出更易于阅读。
  2. A 类中的 ping 和 pong 方法都委托给 super()。
  3. B 类中的 只有ping 方法委托给 super()。
  4. Leaf 类只实现了 ping,并委托给 super()。

现在让我们看看在 Leaf 的实例上调用 ping 和 pong 方法的效果。

例 14-5。用于在 Leaf 对象上调用 ping 和 pong 的 Doctests。

    >>> leaf1 = Leaf()  1>>> leaf1.ping()    2<instance of Leaf>.ping() in Leaf<instance of Leaf>.ping() in A<instance of Leaf>.ping() in B<instance of Leaf>.ping() in Root>>> leaf1.pong()   3<instance of Leaf>.pong() in A<instance of Leaf>.pong() in B
  1. Leaf1 是 Leaf 的一个实例
  2. 调用 Leaf1.ping() 会激活 Leaf、A、B 和 Root 中的 ping 方法,因为前三个类中的 ping 方法调用 super().ping()。
  3. 调用leaf1.pong() 通过继承激活A 中的pong,然后调用super.pong() 激活B.pong。

示例 14-5 和图 14-1 中显示的激活序列由两个因素决定:

  1. Leaf 类的方法解析顺序。
  2. 在每个方法中使用 super()。

每个类都有一个名为 __mro__ 的属性,其中包含一个按方法解析顺序对超类的引用的元组,从当前类一直到Object类。对于 Leaf 类,这是它的 __mro__:

>>> Leaf.__mro__  # doctest:+NORMALIZE_WHITESPACE(<class 'diamond1.Leaf'>, <class 'diamond1.A'>, <class 'diamond1.B'>,<class 'diamond1.Root'>, <class 'object'>)

NOTE:

查看图 14-1,您可能认为 MRO 描述了广度优先搜索,但这对于特定的类层次结构来说只是巧合。MRO 是由一种称为 C3 的已发布算法计算的。它在 Python 中的使用在 Michele Simionato 的  The Python 2.3 Method Resolution Order. 中有详细说明。这是一篇具有挑战性的文章,但 Simionato 写道:“除非你充分利用多重继承并且类的层次结构非常复杂,否则你不需要理解 C3 算法,你可以轻松地跳过这篇论文。”

MRO 只决定调用顺序,但是否会在每个类中激活特定方法取决于每个实现是否调用 super()。

下面是使用 pong 方法的实验。 Leaf 类没有覆盖它,因此调用 Leaf1.pong() 会激活 Leaf.__mro__ 的下一个类中的实现:A 类。方法 A.pong 调用 super().pong()。 B 类是 MRO 中的下一个类,因此 B.pong 被激活。但是B.pong 没有调用 super().pong(),所以激活序列到这里结束。

MRO 不仅考虑继承图,还考虑在子类声明中的超类列出顺序。换句话说,如果在 diamond.py(示例 14-4)中将 Leaf 类声明为 Leaf(B, A),那么类 B 出现在 Leaf.__mro__ 中的 顺序在类A 之前。这会影响ping方法的激活顺序,也会导致leaf1.pong()通过继承激活B.pong,但是A.pong和Root.pong永远不会运行,因为B.pong没有调用super( )。

当一个方法调用 super() 时,它是一个协作方法。协作方法支持协作多重继承。这些术语是有意的:为了工作,Python 中的多重继承需要相关方法的协作。在 B 类中,ping 方法选择了合作,但 pong选择了 不合作。

Warning:

未进行协作的方法可能是导致细微错误的原因。大多数阅读示例 14-4 的编码人员可能会认为,当方法 A.pong 调用 super.pong() 时,最终会激活 Root.pong。但是如果 B.pong 在 Root.pong之前被激活, Root.pong就永远不会被激活。这就是为什么建议非根节点类的每个方法 m 都应该调用 super().m() 的原因。

协作方法必须有兼容的签名,因为你永远不会知道 A.ping 会在 B.ping 之前还是之后被调用:激活顺序取决于继承这

两个类的每个子类的声明中 A 和 B 的顺序。

Python 是一种动态语言,因此 super() 与 MRO 的交互也是动态的。示例 14-6 显示了这种动态行为的惊人结果。

例 14-6。 Diamond2.py:展示 super() 动态特性的类。

from diamond import A  1class U():  2def ping(self):print(f'{self}.ping() in U')super().ping()  3class LeafUA(U, A):  4def ping(self):print(f'{self}.ping() in LeafUA')super().ping()
  1. A 类来自 diamond.py(示例 14-4)。
  2. U 类与diamond模块中的类 A 或 Root 没有关联。
  3. super().ping() 有什么作用?答:视情况而定。继续阅读。
  4. LeafUA按照U和A的顺序进行继承。

如果创建 U 的实例并尝试调用 ping,则会出现错误:

    >>> u = U()>>> u.ping()Traceback (most recent call last):...AttributeError: 'super' object has no attribute 'ping'

super()返回的'super'对象没有'ping'属性,因为U的MRO有两个类:U和object,后者没有名为'ping'的属性。

但是,U.ping 方法并非完全没有希望。看一下这个:

    >>> leaf2 = LeafUA()>>> leaf2.ping()<instance of LeafUA>.ping() in LeafUA<instance of LeafUA>.ping() in U<instance of LeafUA>.ping() in A<instance of LeafUA>.ping() in Root>>> LeafUA.__mro__  # doctest:+NORMALIZE_WHITESPACE(<class 'diamond2.LeafUA'>, <class 'diamond2.U'>,<class 'diamond.A'>, <class 'diamond.Root'>, <class 'object'>)

LeafUA 中的 super().ping() 调用激活了 U.ping,它也通过调用 super().ping() 进行协作,激活 A.ping,并最终激活 Root.ping。

注意 LeafUA 的基类是按照 (U, A) 的顺序。如果是顺序相反的基类(A, U),那么leaf2.ping() 将永远不会到达 U.ping,因为 A.ping 中的 super().ping() 会激活 Root.ping,并且该方法不会调用 super ()。

在实际的编程中,像 U 这样的类是一个 mixin 类:一个旨在与多重继承中的其他类一起使用的类,以提供额外的功能。我们很快就会在“混合类”中学习。

为了结束对 MRO 的讨论,图 14-2 说明了 Python 标准库中 Tkinter GUI 工具包的复杂多重继承图的一部分。

要研究这张图,请从底部的 Text 类开始。 Text 类实现了一个功能齐全的多行可编辑文本小组件。它本身提供了丰富的功能,但也继承了其他类的许多方法。左侧显示了一个简单的 UML 类图。在右侧,它标注了有显示 MRO 的箭头,如在 print_mro 便利函数的帮助下列出的:

>>> def print_mro(cls):
...     print(', '.join(c.__name__ for c in cls.__mro__))
>>> import tkinter
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

现在让我们谈谈mixin。

Mixin 类

一个 mixin 类被设计为与至少一个其他类以多重继承一起被子类化。mixin 不应该是具体类的唯一基类,因为它不提供具体对象的所有功能,而只是添加或自定义子类或兄弟类的行为。
Note:

Mixin 类是一个约定,在 Python 和 C++ 中没有明确的语言支持。Ruby 允许显式定义和使用作为mixin的模块——可以包含方法的集合以向类添加功能。 C#、PHP 和 Rust 实现了 trait,这也是 mixin 的一种显式形式。

让我们看一个简单但方便的 mixin 类示例。

不区分大小写的映射

示例 14-8 展示了 UpperCaseMixin,该类旨在通过在添加或查找这些键时将这些键大写来提供对带有字符串键的映射的不区分大小写的访问。

import collectionsdef _upper(key):  1try:return key.upper()except AttributeError:return keyclass UpperCaseMixin:  2def __setitem__(self, key, item):super().__setitem__(_upper(key), item)def __getitem__(self, key):return super().__getitem__(_upper(key))def get(self, key, default=None):return super().get(_upper(key), default)def __contains__(self, key):return super().__contains__(_upper(key))
  1. 这个辅助函数接受任何类型的键,并尝试返回 key.upper();如果失败,它将返回未更改的key
  2. mixin 实现了四种基本的映射方法,这些方法都调用了 super(),如果可以就将键大写。

由于 UpperCaseMixin 的每个方法都调用 了super(),因此这个 mixin 依赖于实现或继承具有相同签名的方法的兄弟类。为了能够让自己起作用,mixin 通常需要出现在继承它的子类的 MRO 中的其他类之前。实际上,这意味着 mixin 必须首先出现在类声明的基类元组中。这里有两个例子:

例 14-9。 uppermixin.py:两个使用 UpperCaseMixin 的类。

class UpperDict(UpperCaseMixin, collections.UserDict):  1passclass UpperCounter(UpperCaseMixin, collections.Counter):  2"""Specialized 'Counter' that uppercases string keys"""  3
  1. UpperDict 不需要自己的实现,但 UpperCaseMixin 必须是第一个基类,否则将先调用 UserDict 中的方法。
  2. UpperCaseMixin 也适用于 Counter。
  3. 最好提供一个 docstring 来满足 class 语句语法中对主体的需求,而不是 pass。

这里有一些来自 uppermixin.py 的 doctest,对于 UpperDict:

 >>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])>>> list(d.keys())['A', 2]>>> d['b'] = 'letter B'>>> 'b' in dTrue>>> d['a'], d.get('B')('letter A', 'letter B')>>> list(d.keys())['A', 2, 'B']

以及 UpperCounter 的快速演示:

    >>> c = UpperCounter('BaNanA')>>> c.most_common()[('A', 3), ('N', 2), ('B', 1)]

UpperDict 和 UpperCounter 看起来几乎很神奇,但我必须仔细研究 UserDict 和 Counter 的代码才能使 UpperCaseMixin 与它们一起工作。

例如,我的第一个 UpperCaseMixin 版本没有提供 get 方法。该版本适用于 UserDict 但不适用于 Counter。 UserDict 类从 collections.abc.Mapping 继承了 get,而那个 get 调用了我实现的 __getitem__。但是当 UpperCounter 在 __init__ 上进行初始化时,键就不是大写的。这是因为 Counter.__init__ 使用 Counter.update,而后者又依赖于从 dict 继承的 get 方法。但是dict类中的get方法并没有调用__getitem__。这是“标准库中 __missing__ 的使用不一致”中讨论的问题的核心。它也清楚地提醒我们利用继承的程序的脆弱性和令人费解的本质,即使是小规模的。

下一节将介绍多重继承的几个示例,通常使用 mixin 类的特性。

多重继承的真实应用

在 Design Patterns book 中,几乎所有的代码都是用 C++ 编写的,但多重继承的唯一例子是 适配器模式。在 Python 中,多重继承也不是经常出现,但我将在本节中讲述一些重要的例子。

ABC 也是 Mixin

在 Python 标准库中,最常使用多重继承用法是 collections.abc 包。这没有争议:毕竟,即使 Java 也支持接口的多重继承,而且 ABC 是接口声明,可以选择提供具体的方法实现。

Python 的 collections.abc 官方文档使用术语 mixin 方法来表示在许多集合 ABC 中实现的非抽象方法。提供 mixin 方法的 ABC 扮演两个角色:它们提供了接口定义同时也是 mixin 类。例如,collections.UserDict 的实现依赖于 collections.abc.MutableMapping 提供的几个 mixin 方法。

ThreadingMixin 和 ForkingMixin

http.server 包提供了 HTTPServer 和 ThreadingHTTPServer 类。后者是在 Python 3.7 中添加的。它的文档是这样说的:

class http.server.ThreadingHTTPServer(server_address, RequestHandlerClass)

此类与 HTTPServer 是相同的,但是使用线程来处理请求,这利用了ThreadingMixIn。这对于处理 Web 浏览器预打开套接字很有用,如果是HTTPServer的话,它将会无限期等待。

这是 Python 3.10 中 ThreadingHTTPServer 类的完整源代码:

class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):daemon_threads = True

socketserver.ThreadingMixIn 的源代码有 38 行,包括注释和文档字符串。以下是其实现的摘要:

例 14-10。 Python 3.10 中 Lib/socketserver.py 的一部分。

class ThreadingMixIn:"""Mix-in class to handle each request in a new thread."""# 8 lines omitted in book listingdef process_request_thread(self, request, client_address):  1... # 6 lines omitted in book listingdef process_request(self, request, client_address):  2... # 8 lines omitted in book listingdef server_close(self):  3super().server_close()self._threads.join()
  1. process_request_thread 没有调用 super() 因为它是一个新方法,而不是覆盖基类的方法。它的实现调用了 HTTPServer 提供或继承的三个实例方法。
  2. 这会覆盖 HTTPServer 从 socketserver.BaseServer 继承的 process_request 方法,启动一个线程并将实际工作委托给在该线程中运行的 process_request_thread。
  3. server_close 调用 super().server_close() 并停止接受请求,然后等待 process_request 启动的线程完成它们的工作。

ThreadingMixIn 出现在 ForkingMixin 旁边的 socketserver 模块文档中。后者旨在支持基于 os.fork() 的并发服务器,os.fork() 是一种用于启动子进程的 API,可在符合 POSIX 的类 Unix 系统中使用。

Django 通用视图Mixin

Note:

您无需了解 Django 即可阅读本节内容。我使用框架的一小部分作为多重继承的实际示例,假设您有使用任何语言或框架进行服务器端 Web 开发的一些经验,我将尝试提供所有必要的背景知识。

在 Django 中,视图是一个可调用对象,它接受一个request参数——一个表示 HTTP 请求的对象——并返回一个表示 HTTP 响应的对象。不同的响应是我们在这次讨论中所感兴趣的。它们可以像没有内容正文的重定向响应一样简单,也可以像在线商店中的目录页面那么复杂,从 HTML 模板呈现并列出多个带有购买按钮和详细信息页面链接的商品。

最初,Django 提供了一组称为通用视图的函数,用于实现一些常见用例。例如,许多网站需要显示包含大量元素信息的搜索 .png,列表跨越多个页面,并且每个项都有一个链接到包含有关它的详细信息的页面。在 Django 中,列表视图和详细信息视图旨在协同工作来解决下面这个问题:列表视图呈现搜索结果,详细信息视图为每个单独的项生成一个页面。

然而,最初的通用视图是函数,所以它们不可扩展。如果你需要做一些类似但不完全像通用列表视图的事情,你就不得不自己完成。

基于类的视图的概念是在 Django 1.3 中引入的,以及一组通用的视图类,这些类组织为基类、mixin 和拿来即用的具体类。在 Django 3.2 中,基类和 mixin 位于 django.views.generic 包的基本模块中,如图 14-3 所示。在图的顶部,我们看到两个职责非常不同的的类:View 和 TemplateResponseMixin。

TIP:

Classy Class-Based Views 是学习这些类的一个很好的资源,您可以在其中轻松浏览它们,查看每个类中的所有方法(继承的、覆盖的和添加的方法)、查看图表、浏览他们的文档并跳转到他们在 GitHub 上的源代码。

View 是所有视图的基类(可以是 ABC),它提供了像 dispatch 方法这样的核心功能,它委托给“handler”方法,如 get、head、post 等,由具体的子类实现以处理不同的 HTTP 动词。View 的具体子类应该实现handler方法,那么为什么这些方法不是 View 接口的一部分呢?原因:子类可以自由实现他们想要支持的handler.TemplateView 只用于显示内容,所以它只实现了 get。如果将 HTTP POST 请求发送到 TemplateView,则继承的 View.dispatch 方法会检查是否有实现post handler,并生成 HTTP 405 Method Not Allowed 响应。

TemplateResponseMixin 提供的功能只对需要使用模板的视图感兴趣。 例如,一个 RedirectView 没有响应的内容体,所以它不需要模板,也继承这个 mixin 。TemplateResponseMixin 为 TemplateView 和其他模板渲染视图提供行为,如 ListView、DetailView 等,定义在 django.views.generic 子包中。图 14-4 描述了 django.views.generic.list 模块和base模块的一部分。

对于 Django 用户来说,图 14-4 中最重要的类是 ListView,它是一个聚合类,没有任何代码(它的主体只是一个文档字符串)。实例化时,ListView 具有 object_list 实例属性,模板可以通过该属性进行迭代以显示页面内容,通常是返回多个对象的数据库查询结果。与生成此可迭代对象相关的所有功能都来自 MultipleObjectMixin。该 mixin 还提供了复杂的分页逻辑——在一个页面中显示部分结果并链接到更多页面。

假设您要创建一个视图,该视图不会呈现模板,但会生成 JSON 格式的对象列表。这就是 BaseListView 存在的原因。它提供了一个易于使用的扩展点,将 View 和 MultipleObjectMixin 功能结合在一起,而没有模板机制的开销。

Django 基于类的视图 API 是比 Tkinter 更好的多重继承示例。特别是,它的 mixin 类很容易理解:每个类都有明确定义的用途,并且它们都以 ...Mixin 后缀命名。

Django 用户并未普遍接受基于类的视图。许多人以有限的方式使用它们,就像一个黑盒子,但是当需要创建新的东西时,许多 Django 编码人员继续编写负责所有这些职责的整体视图函数,而不是尝试重用基本视图和mixin。

学习如何利用基于类的视图以及如何扩展它们以满足特定的应用程序需求确实需要一些时间,但我发现研究它们是值得的:他们消除了大量样板代码,更容易重用解决方案,甚至改善团队沟通。例如,通过为模板和传递给模板上下文的变量定义标准名称。基于类的视图是“在轨道上”的 Django 视图。

Tkinter 中的多重继承

Python 标准库中多重继承的一个极端例子是 Tkinter GUI 工具包。我使用了 Tkinter 小组件层次结构的一部分来说明图 14-2 中的 MRO。图 14-5 显示了 tkinter 基础包中的所有小组件类(tkinter.ttk 子包中有更多小组件)。

我写这篇文章时,Tkinter 已经 25 岁了。它不是当前最佳实践的示例。但它显示了当程序员不了解其缺点时,他们会如何使用多重继承。 当我们在下一节介绍一些好的做法时,它将作为一个反例。

考虑图 14-5 中的这些类:

  1. Toplevel:Tkinter 应用程序中顶级窗口的类。
  2. Widget:可以放置在窗口上的每个可见对象的超类。
  3. Button:一个普通的按钮小组件。
  4. Entry:单行可编辑文本字段。
  5. Text:多行可编辑文本字段。

以下是这些类的 MRO,由示例 14-7 中的 print_mro 函数显示:

>>> 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

Note:

按照目前的标准,Tkinter 的类层次结构非常深。Python 标准库的几个部分很少有超过 3 或 4 级的具体类,Java 类库也是如此。然而,有趣的是,Java 类库中一些最深的层次结构正是在与 GUI 编程相关的包中:java.awt 和 javax.swing。Squeak 是 Smalltalk 的现代免费版本,包括强大且创新的 Morphic GUI 工具包,还是具有很深的类层次结构。根据我的经验,GUI 工具包是继承最有用的地方。

请注意这些类与其他类的关系:

  1. Toplevel 是唯一一个不继承自 Widget 的图形类,因为它是顶级窗口并且不像小组件那样表现——例如,它不能附加到窗口或窗体上。Toplevel 继承自 Wm,它提供宿主窗口管理器的直接访问功能,如设置窗口标题和配置其边框。
  2. Widget 直接继承了BaseWidget ,还继承了 Pack、Place 和 Grid 。最后三个类是几何管理器:它们负责在窗口或窗体内排列小组件。每个类封装了不同的布局策略和小组件放置的 API。
  3. Button 与大多数小组件一样,仅继承自 Widget,但间接继承自 Misc,后者为每个小组件提供了数十种方法。
  4. Entry 继承自 Widget 和 XView,XView支持水平滚动。
  5. Text继承自 Widget、XView 和 YView,YView 支持垂直滚动。

我们现在将讨论多重继承的一些良好实践,看看 Tkinter 是否也遵循这些实践。

处理继承

Alan Kay 在题词中所写的仍然是正确的:仍然没有可以指导程序员实践的关于继承的完整的理论。我们拥有的是经验法则、设计模式、“最佳实践”、巧妙的首字母缩略词、禁忌等。其中一些提供了有用的指导方针,但没有一个被大众普遍接受或始终适用于所有场景。

使用继承很容易创建难以理解和脆弱的设计,即使没有多重继承。因为我们没有一个全面的理论,这里有一些避免把类图做成意大利面条那样混乱的技巧。

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

本小节的标题是《设计模式》一书中的面向对象设计的第二个原则,这 是我在这里可以提供的最佳建议。一旦你习惯了继承,就很容易过度使用它。以整齐的层次结构放置对象会吸引我们的秩序感;程序员这样做只是为了好玩。

使用组合的设计技巧会带来更灵活的设计。例如,在 tkinter.Widget 类的情况下,我们可以不从所有几何管理器继承方法,而是让小组件实例可以保存对几何管理器的引用,并调用它的方法。毕竟,Widget 不应该“成为”几何管理器,而是可以通过委托使用几何管理器的服务。然后您可以设计一个新的几何管理器类,而无需影响小部件类层次结构,也无需担心名称冲突。即使是单继承,这个原则也增强了灵活性,因为继承是一种紧密耦合的形式,而高大的继承树往往是脆弱的。

组合和委托可以代替 mixin类的作用 ,但不能代替使用接口继承来定义类型的层次结构。

2. 理解为什么在每种情况下都使用继承

在处理多重继承时,直截了当地说明为什么在每个特定情况下使用继承的原因。主要原因是:

  • 接口的继承创建了一个子类型,表明了一种“is-a”关系。这最好用 ABC 来完成。
  • 继承实现,通过复用来避免代码重复。 Mixins 可以帮助解决这个问题。

在实践中,这两种用法通常是同时出现,但只要你能明确意图,那就可以做。代码复用导致的继承是一个实现细节,通常可以用组合和委托来代替。另一方面,接口继承是框架的支柱。如果可能,接口继承应该只使用 ABC 作为基类。

3. 使用 ABC 显示定义接口

在现代 Python 中,如果一个类打算定义一个接口,那么这个类应该是一个显式的 ABC 或一个 Typing.Protocol 子类。ABC 应仅子类化 abc.ABC 或其他 ABC。 ABC 的多重继承不会带来问题。

4. 使用显式 Mixins 进行代码重用

如果一个类旨在为多个不相关的子类提供可重用的方法实现,而不是表明“is-a”关系,那么它应该是一个显式的 mixin 类。从概念上讲,mixin 并没有定义新的类型。它只是绑定了需要进行重用的方法。每个 mixin 都应该提供一个特定的行为,实现数量不多且非常密切相关的方法。Mixin 应该避免持有任何内部状态——即mixin 类不应具有实例属性。

在 Python 中没有正式的方式来声明一个类是一个 mixin,所以强烈建议用 Mixin 后缀命名类。

5. 向用户提供聚合类

如果一个类主要通过从 mixin 的继承构造的。并且类不添加自己的结构或行为,那么这个类被称为聚合类。

--Grady Booch et al.

例如,这是图 14-4 右下角的 Django ListView 类的完整源代码。

class ListView(MultipleObjectTemplateResponseMixin, BaseListView):"""Render some list of objects, set by `self.model` or `self.queryset`.`self.queryset` can actually be any iterable of items, not just a queryset."""

ListView 的主体是空的,但该类提供了一个有用的服务:它将一个Mixin和一个应该一起使用的基类组合在一起。

另一个例子是 tkinter.Widget,它有四个基类,没有自己的方法或属性——类体中只是一个文档字符串。多亏了 Widget 聚合类,我们可以使用所需的 mixin 创建新的小组件,而无需弄清楚它们应该以何种顺序声明以按预期工作。

请注意,聚合类不必类体完全为空,但它们通常类体是空的。

6. 只继承为子类化而设计的类

在对本章的一个评论中,技术审阅者 Leonardo Rochael 提出了这个警告:

Warning:

集成任何复杂类并覆盖其方法很容易出错,因为超类方法可能会以意想不到的方式忽略子类覆盖。尽可能避免覆盖超类的方法,或者至少只对对设计为易于扩展的类进行子类化,并且只能以它们设计扩展的方式进行子类化。

这是一个很好的建议,但我们如何知道一个类是否或如何被设计为可扩展的?

第一个答案是文档(有时以文档字符串甚至代码中的注释的形式)。例如,Python 的 socketserver 包被描述为“网络服务器框架”。顾名思义,它的 BaseServer 类是为子类化而设计的。更重要的是,类的源代码中的文档和文档字符串明确指出其哪些方法可以被子类覆盖。

在 Python ≥ 3.8 中,PEP 591—Adding a final qualifier to typing 提供了一种明确设计约束的新方法。PEP 引入了一个 @final 装饰器,可以应用于类或单个方法,以便 IDE 或类型检查器可以报告对这些类进行子类化或覆盖这些方法的错误尝试。

7. 避免继承多个具体类

子类化具体类比子类化 ABC 和 mixin 更危险,因为具体类的实例通常具有内部状态,当您覆盖依赖于该状态的方法时,这些状态很容易被破坏。即使您的方法通过调用 super() 进行协作,并且使用 __x 语法将内部状态保存在私有属性中,方法覆盖仍然有无数种方式会引入错误。

在“Waterfowl and ABCs”中,Alex Martelli 引用了 Scott Meyer 的 More Effective C++,其中说:“所有非叶节点类都应该是抽象的”。换句话说,Meyer 建议只对抽象类进行子类化。

如果您必须使用继承进行代码重用,那么用于重用的代码应该在 ABC 的 mixin 方法中或在显式命名的 mixin 类中。

我们现在将从这些建议的角度分析 Tkinter。

Tkinter:好的、不好的和令人厌恶的方面

Note:

请记住,自 1994 年 Python 1.1 发布以来,Tkinter 一直是标准库的一部分。Tkinter 的底层是 Tcl 语言的优秀 Tk GUI 工具包。Tcl/Tk 组合最初不是面向对象的,因此 Tk API 基本上是一个庞大的函数目录。但是,如果不是在其原始实现使用了 Tcl ,该工具包的设计理念中是面向对象的。

Tkinter 没有遵循上一节中的大多数建议,除了“5. Provide Aggregate Classes to Users”。即便如此,这也不是一个很好的例子,因为组合可能会更好地将几何管理器集成到 Widget 中,如“1. Favor Object Composition Over Class Inheritance”。

tkinter.Widget 的文档字符串以“内部类”字样开头。这表明 Widget 应该是一个 ABC。它传达的信息是:“除了所有三个几何管理器的全部方法之外,您还可以依靠每个 Tkinter Widget提供基本的小组件方法(__init__、destroy 和数十个 Tk API 函数)。”我们可以同意这不是一个很好的接口定义(它太宽泛了),但它是一个接口,而 Widget 将它“定义”为它的超类接口的集合。

封装了 GUI 应用程序逻辑的 Tk 类继承自 Wm 和 Misc,这两个基类既不是抽象的也不是 mixin(Wm 不是一个合适的 mixin,因为 TopLevel 的超类只有Wm)。Misc 类的名称本身就是一种非常强烈的代码异味。Misc 有 100 多个方法,所有小组件都继承自它。为什么每个小组件都需要有用于剪贴板处理、文本选择、计时器管理等的方法?您无法按钮中进行粘贴或从滚动条中选择文本。 Misc 应该被分成几个专门的 mixin 类,并不是所有的小组件都应该继承自所有的 mixin 。

公平地说,作为 Tkinter 用户,您根本不需要知道或使用多重继承。它是隐藏在小组件类后面的实现细节,您只需要自己的代码中实例化或子类化小组件。但是当您键入 dir(tkinter.Button) 并尝试在列出的 214 个属性中找到您需要的方法时,您将承受过度使用多重继承的后果。如果您决定实现一个新的 Tk 小组件,您将需要面对Widget的复杂性。

TIP:

尽管存在一些问题,但如果您使用 tkinter.ttk 包及其主题小组件时,Tkinter 是稳定、灵活的,并提供现代外观和感觉。此外,一些原始小部件,如 Canvas 和 Text,非常强大。您可以在几个小时内将 Canvas 对象打造成一个简单的拖放绘图应用程序。如果您对 GUI 编程感兴趣,Tkinter 和 Tcl/Tk 绝对值得一看。

第 十四 章 继承:究竟是好是坏相关推荐

  1. 第二十四章 并发编程

    第二十四章 并发编程 爱丽丝:"但是我不想进入疯狂的人群中" 猫咪:"oh,你无能为力,我们都疯了,我疯了,你也疯了" 爱丽丝:"你怎么知道我疯了&q ...

  2. 山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将...

    山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将 "我是第一次--请你,请你温柔一点--"少女一边娇喘着,一边将稚嫩的红唇紧贴在男子耳边,樱桃小嘴盈溢 ...

  3. 系统架构师学习笔记_第十四章_连载

    第十四章  基于ODP的架构师实践 14.1  基于ODP的架构开发过程 系统架构 反映了功能在系统系统构件中的 分布.基础设施相关技术.架构设计模式 等,它包含了架构的 原则 和 方法.构件关系 与 ...

  4. 第二十四章 异常和错误处理 1异常

    // 第二十四章 异常和错误处理 //1异常 /*#include <iostream> using namespace std; class wrong{}; void error() ...

  5. 《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

    <深入理解 Spring Cloud 与微服务构建>第十四章 服务链路追踪 Spring Cloud Sleuth 文章目录 <深入理解 Spring Cloud 与微服务构建> ...

  6. 【JAVA SE】第十四章 集合框架、语法糖和泛型

    第十四章 集合框架.语法糖和泛型 文章目录 第十四章 集合框架.语法糖和泛型 一.集合框架 1.概念 2.接口 二.语法糖 1.概念 2.解语法糖 三.泛型 1.概念 2.泛型类 3.泛型接口 4.泛 ...

  7. 《操作系统真象还原》第十四章 ---- 实现文件系统 任务繁多 饭得一口口吃路得一步步走啊(上二)

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 闲聊时刻 部分缩写熟知 实现文件描述符的原理 文件描述符的介绍 文件描述符与inode的介绍 文件描述符与PCB的描述符数组的介绍 实现文件操 ...

  8. 《Dreamweaver CS6 完全自学教程》笔记 第十四章:使用 CSS 设计网页

    文章目录 第十四章:使用 CSS 设计网页 14.1 CSS 样式表简介 14.2 CSS 的基本语法 14.3 伪类.伪元素以及样式表的层叠顺序 14.3.1 伪类和伪元素 14.3.2 样式表的层 ...

  9. matlab的meadian函数_24 第二十四章 时间序列模型_W

    <24 第二十四章 时间序列模型_W>由会员分享,可在线阅读,更多相关<24 第二十四章 时间序列模型_W(31页珍藏版)>请在人人文库网上搜索. 1.第二十四章时间序列模型 ...

最新文章

  1. Kali Linux 官方宣传视频
  2. springcloud都有什么组件?这个列表不得不看!
  3. Android仿QQ5.0侧滑菜单ResideMenu的使用和源码分析
  4. nltk book的下载
  5. 缩减oracle日志,[20180829]减少日志生成量.txt
  6. php防止跨域提交,PHP防止跨域提交表单的简单示例
  7. slim框架中防止crsf攻击时,用到的函数hash_equals
  8. 安卓桌面整理app_升级到 iOS 13,你还会删除 APP 和整理桌面了吗?
  9. 遮罩层 fixed 在 ie 里无法显示
  10. Gartner:全球晶圆代工市场排行榜?台积电保持第一、联电退居第三
  11. 和机器人问问题的软件_如何开发一个特定领域的自动问答机器人(Chat Bot)?
  12. 今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!
  13. Android视频教程基础篇(现场版)_张凌华老师主讲
  14. 数据结构算法(2)--字符串匹配
  15. 网页设计作业 / 动漫网页设计作业,网页设计作业 / 动漫网页设计成品,网页设计作业 / 动漫网页设计成品模板下载
  16. 大学计算机应用基础屈立成,五笔字型输入法教程-计算机应用基础教学网.PDF
  17. 自然语言处理的通俗百科
  18. 莱佛士毕业生 Amos YEO与快餐巨头KFC街头服饰合作系列
  19. 一个大龄FPGA工程师在CSDN发的第一篇博客
  20. VScode实现抖音功能

热门文章

  1. java有substr方法_java substring和substr
  2. 03 TI OMAPL138E Linux移植 (Davinci) (资源获取+从串口启动UBoot+从网络启动Linux与文件系统)
  3. 编程的本质是逻辑性思维
  4. 老男孩mysql运维dba实战21部完整版_老男孩MySQL DBA 运维课程全套,资源教程下载...
  5. 抽象代数之第一群同构定理的证明
  6. 常用的排序算法-快速记忆
  7. Google全系列产品不再信任赛门铁克某款根证书-转载
  8. zk4元年拆解_科比zk4复刻前掌没有zoom zk4选秀日复刻中底拆解测评
  9. 毕业设计 自制移动机器人,三维零件设计(SolidWorks三维分享)
  10. 第3节 三个败家子(3)——被忽略的刘备之子