Skip to content

Latest commit

 

History

History
executable file
·
590 lines (388 loc) · 44.1 KB

12章-继承该如何是好.md

File metadata and controls

executable file
·
590 lines (388 loc) · 44.1 KB

Inheritance: for good or for worse 第十二章 继承该如何是好

[我们]推动了继承思想,使其成为新手也可以构建框架的一种方法,而原先只有专家才可以设计的框架。 — 阿兰。凯《Smalltalk的早期历史》

本章有关于继承和子类化,其中有两个针对Python不同的重点内容:

  • 从内建类型中的子类化陷阱
  • 多重继承以及方法解析顺序

很多人认为多重继承带来的麻烦远大于其自身带来好处。

然而,由于Java特别的成功及其带来的影响力,这就意味着,在实际操作中很多程序员并没有见到多重继承。这就是为什么我们通过两个重要的项目来阐明多重继承的适应范围:Tkinter GUI套件,以及Django web 框架。

我们从内建子类化的问题开始。余下的章节会用案例研究来学习多重继承,并讨论在构建类的分层设计时所遇到的问题。

技巧之-子类化内建类型

在Python2.2之前,子类化list或者dict这样的内建类型是不可能的。打那以后,Python虽然可以做到子类化内建类型,但是仍然要面对的重要警告是:内建的代码(由C语言重写)并不会调用被通过用户自定义类所覆盖的特殊方法。

对问题的准确描述都放在了PyPy文档,以及内建类型的子类化一节中的PyPy和CPython之间差异

正式地来说,Cpython对完全地重写内建类型的子类方法时是否要显式调用上显得毫无规则可言。大略上,这些方法从来没有被其他的相同对象的内建方法所调用。例如,dict子类中的重写__getitem__()不会被get()这样的内建方法调用。

例子12-1则说明了此问题。

例子12-1。重写的__setitem__dict__init____update__方法所忽略。


>>> class DoppelDict(dict):
...     def __setitem__(self, key, value):
...         super(DoppelDict, self).__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:dictupdate方法也没有使用我们定义的__setitem__:值'three'没有被重复。

该内建行为违反了面向对象的基本准则:方法的搜索应该总是从目标实例(self)的类开始,甚至是调用发生在以超类实现的方法之内部。在这样的悲观的情形下,

问题是在一个实例内部没有调用的限制,例如,不论self.get()是否调用self.__getitem__(),都会出现会被内建方法所调用其他类的方法被重写。下面是改编自PyPy文档的例子:

例子12-2。AnswerDict__getitem__dict.update所忽略。

>>> 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:AnserDict.__getitem__总是返回42,不论键是什么。

2:ad是一个带有键值对('a', 'foo')AnswerDict

3:ad['a']如所期望的那样返回42。

4:d是一个普通使用ad更新的dict实例。

5:dict.update方法忽略了AnserDict.__getitem__

警告

直接地子类化类似dict或者list或者str这样的内建类型非常容易出错,因为大多数的内建方法会忽略用户所定义的重写方法。你应该从被设计成易于扩展的collections模块的UserDictUserListUserString派生类,而不是子类化内建对象。

如果你子类化collections.UserDict而不是dict,那么例子12-1和例子12-2中的问题都会被该解决。见例子12-3。

例子12-3。DoppelDict2AnswerDict2一如所希望的运行,因为它们扩展的是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-8中StrKeyDict类。继承自collections.UserDict的原始版本,由三种方法实现:__missing_____contains____setitem__

总结:本节所描述的问题仅应用于在C语言内的方法委托实现内建类型,而且仅对用户定义的派生自这些的类型的类有效果。如果你在Python中子类化类编程,比如,UserDict或者MutableMapping,你不会遇到麻烦的。

还有问题就是,有关继承,特别地的多重继承:Python如何确定哪一个属性应该使用,如果超类来自并行分支定义相同的名称的属性,答案在下面一节。

多重继承以及方法解析顺序

当不关联的祖先类实现相同名称的方法时,任何语言实现多重继承都需要解决潜在的命名冲突。这称做“钻石问题”,一如图表12-1和例子12-4所描述。

图片:略

图表12-1.左边:UML类图表阐明了“钻石问题”。右边:虚线箭头为例子12-4描绘了Python MRO(方法解析顺序).
例子12-4. diamond.py:类A,B, C,和D构成了图表12-1中的图。

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)

注意类BC都实现了pong方法。唯一的不同是C.pong输出大写的单词PONG

如果你对实例D调用d.pong(),实际上哪一个pong方法会运行呢?对于C++程序员来说他们必须具有使用类名称调用方法,以解决这个模棱两可的问题。这样的问题在Python中也能够解决。看下例子12-5就知道了。

例子12-5.对类D的实例的pong方法调用的两种形式。

>>> from diamond import *
>>> d = D()
>>> d.pong()  #  1
pong: <diamond.D object at 0x10066c278>
>>> C.pong(d)  #  2
PONG: <diamond.D object at 0x10066c278>

1: 简单地调用d.pong导致B的运行。
2: 你可以总是直接地对调用超类的方法,传递实例作为明确的参数。

d.pong()这样的模棱两可的调用得以解决,因为Python在穿越继承图时,遵循一个特定的顺序。这个顺序就叫做MRO:方法解析顺序。类有一个被称为__mro__的属性,它拥有使用MRO顺序的超类的引用元组,即,当前的类的所有到object类的路径。拿类D来说明什么是__mro__(参见 图表12-1):

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

推荐的调用超类的委托方法就是内建的super()函数,这样做是因为在Python3中较易使用,就像例子12-4中的类D的pingpong方法所阐述的那样。不过,有时候忽略MRO,对超类直接地调用方法也是也可以的,而且很方便。例如,D.ping方法可以这样写:

    def ping(self):
        A.ping(self)  # instead of super().ping()
        print('post-ping:', self)

注意,当调用直接调用一个类的实例时,你必须明确地传递self,因为你访问的是unbound method

不过,这是最安全的而且更未来化的使用super(),特别是在调用一个框架的方法时,或者任何不受你控制的类继承时。例子12-6演示了在调用方法时super()对MRO的遵循。

例子12-6。使用super()去调用ping(源码见例子12-4)。

>>> from diamond import D
>>> d = D()
>>> d.ping()  # 1
ping: <diamond.D object at 0x10cc40630>  # 2
post-ping: <diamond.D object at 0x10cc40630>  # 3

1: The ping of D makes two calls:
1: D的ping进行了两次调用:

2: The first call is super().ping(); the super delegates the ping call to class A; A.ping outputs this line.
2: 第一次调用了super().ping();super委托ping去调用类A;A.ping输出内容。

3: The second call is print('post-ping:', self) which outputs this line.
3: 第二次调用的是print('post-ping:', self)输出本行内容。

Now let’s see what happens when pingpong is called on an instance of D.

现在让我们看一看调用D的实例上的pingpong到底发生哪些事情。

Example 12-7. The five calls made by pingpong (source code in Example 12-4)。

例子12-7.由pingpong发起的五次调用(源码见例子12-4)。

>>> from diamond import D
>>> d = D()
>>> d.pingpong()
>>> d.pingpong()
ping: <diamond.D object at 0x10bf235c0> # ①
post-ping: <diamond.D object at 0x10bf235c0>
ping: <diamond.D object at 0x10bf235c0>  # ②
pong: <diamond.D object at 0x10bf235c0>  # ③
pong: <diamond.D object at 0x10bf235c0>  # ④
PONG: <diamond.D object at 0x10bf235c0>  # ⑤

① Call #1 isself.ping() runs the ping method of D, which outputs this line and the next one. ② Call #2 is super.ping() which bypasses the ping in D and finds the ping method in A. ③ Call #3 is self.pong() which finds the B implementation of pong, according to the mro. ④ Call #4 is super.pong() which finds the same B.pong implementation, also following the mro. ⑤ Call #5 is C.pong(self) which finds the C.pong implementation, ignoring the mro.

① 第一次调用的是``self.ping()运行的是D的ping方法,它输出了本行以及下面一行 ② 第二次调用的是super.ping(),它忽略了D中的ping方法然后找到了A中的ping方法。 ③ 第三次调用的是self.ping()它通过mro`找到了B的pong方法。 ④ 第四次调用的是`super.pong()`它同样是通过`mro`找到了B.pong。 ⑤ 第五次调用的是`C.pong(self)`,它忽略了`mro`找到是C.pong。

The MRO takes into account not only the inheritance graph but also the order in which superclasses are listed in a subclass declaration. In other words, if in diamond.py (Example 12-4) the D class was declared as class D(C, B):, the mro of class D would be different: C would be searched before B.

MRO

I often check the__mro__ of classes interactively when I am studying them. Example 12-8 has some examples using familiar classes.

在我研究多重继承时,我常交互式地去检查类的__mro__。例子12-8就使用了熟悉的类。

Example 12-8. Inspecting the __mro__ attribute in several classes 例子 12-8.在多个类中检查__mro__属性

>>> bool.__mro__  # 1
(<class 'bool'>, <class 'int'>, <class 'object'>)
>>> def print_mro(cls):  # 2
...     print(', '.join(c.__name__ for c in cls.__mro__))
...
>>> print_mro(bool)
bool, int, object
>>> from frenchdeck2 import FrenchDeck2
>>> print_mro(FrenchDeck2)  # 3
FrenchDeck2, MutableSequence, Sequence, Sized, Iterable, Container, object
>>> import numbers
>>> print_mro(numbers.Integral)  # 4
Integral, Rational, Real, Complex, Number, object
>>> import io  # 5
>>> print_mro(io.BytesIO)
BytesIO, _BufferedIOBase, _IOBase, object
>>> print_mro(io.TextIOWrapper)
TextIOWrapper, _TextIOBase, _IOBase, object

1: bool inherits methods and attributes from int and object.
2: print_mro produces more compact displays of the MRO.
3: The ancestors of FrenchDeck2 include several ABCs from the collections.abc module.
4: These are the numeric ABCs provided by the numbers module.
5: The io module includes ABCs (those with the …Base suffix) and concrete classes like BytesIO and TextIOWrapper which are the types of binary and text file objects returned by open(), depending on the mode argument.

#####Note The MRO is computed using an algorithm called C3. The canonical paper on the Python MRO explaining C3 is Michele Simionato’s The Python 2.3 Method Resolution Order. If you are interested in the subtleties of the MRO, Further reading has other pointers. But don’t fret too much about this, the algorithm is sensible and Simionato wrote: […] unless you make strong use of multiple inheritance and you have non-trivial hierarchies, you don’t need to understand the C3 algorithm, and you can easily skip this paper.

To wrap up this discussion of the MRO, Figure 12-2 illustrates part of the complex multiple inheritance graph of the Tkinter GUI toolkit from the Python standard library. To study the picture, start at the Text class at the bottom. The Text class implements a full featured, multiline editable text widget. It has rich functionality of its own, but also inherits many methods from other classes. The left side shows a plain UML class dia‐ gram. On the right, it’s decorated with arrows showing the MRO, as listed here with the help of the print_mro convenience function defined in Example 12-8:

>>> import tkinter
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

图片:略

Figure 12-2. Left: UML class diagram of the Tkinter Text widget class and its super‐ classes. Right: Dashed arrows depict Text.mro.

In the next section, we’ll discuss the pros and cons of multiple inheritance, with examples from real frameworks that use it.

接下来的小节中,我们要讨论的是多重继承中的pros和cons,以及在真实框架中使用它的例子。

Multiple Inheritance in the Real World 现实世界中的多重继承

It is possible to put multiple inheritance to good use. The Adapter pattern in the Design Patterns book uses multiple inheritance, so it can’t be completely wrong to do it (the remaining 22 patterns in the book use single inheritance only, so multiple inheritance is clearly not a cure-all).

把多重继承给用好是有可能。设计模式这类书中的适配器模式使用了多重继承,所以也不能完全错误的使用它(书中余下的22个模式仅仅使用的是单继承,因此多重继承明显不是什么灵丹妙药)。

In the Python standard library, the most visible use of multiple inheritance is the collections.abc package. That is not controversial: after all, even Java supports multiple inheritance of interfaces, and ABCs are interface declarations that may optionally pro‐ vide concrete method implementations.[5] .

在Python标准库中,最常见的多重继承是collections.abc包。这并不存在争议:毕竟,

[5].As previously mentioned, Java 8 allows interfaces to provide method implementations as well. The new feature is called Default Methods in the official Java Tutorial.

An extreme example of multiple inheritance in the standard library is the Tkinter GUI toolkit (module tkinter: Python interface to Tcl/Tk). I used part of the Tkinter widget hierarchy to illustrate the MRO in Figure 12-2, but Figure 12-3 shows all the widget classes in the tkinter base package (there are more widgets in the tkinter.ttk sub- package).

多重继承中的一个极端的例子便是标准库中的Tkinter图形套件(tkinter模块:面向Tcl/Tk的Python接口)。

image: ignore

Figure 12-3. Summary UML diagram for the Tkinter GUI class hierarchy; classes tag‐ ged «mixin» are designed to provide concrete methods to other classes via multiple in‐ heritance

Tkinter is 20 years old as I write this, and is not an example of current best practices. But it shows how multiple inheritance was used when coders did not appreciate its drawbacks. And it will serve as a counter-example when we cover some good practices in the next section.

当我写这段文字时Tkinter就已经有20的历史了,它也不是目前最佳实践的例子。

Consider these classes from Figure 12-3:

➊ Toplevel: The class of a top-level window in a Tkinter application.
➋ Widget: The superclass of every visible object that can be placed on a window.
➌ Button: A plain button widget.
➍ Entry: A single-line editable text field.
➎ Text: A multiline editable text field.

Here are the MROs of those classes, displayed by the print_mro function from Example 12-8:

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

Things to note about how these classes relate to others:

• Toplevel is the only graphical class that does not inherit from Widget, because it is the top-level window and does not behave like a widget—for example, it cannot be attached to a window or frame. Toplevel inherits from Wm, which provides direct access functions of the host window manager, like setting the window title and configuring its borders.

• Widget inherits directly from BaseWidget and from Pack, Place, and Grid. These last three classes are geometry managers: they are responsible for arranging widgets inside a window or frame. Each encapsulates a different layout strategy and widget placement API.

• Button, like most widgets, descends only from Widget, but indirectly from Misc, which provides dozens of methods to every widget.

• Entry subclasses Widget and XView, the class that implements horizontal scrolling. • Text subclasses from Widget, XView, and YView, which provides vertical scrolling functionality.

We’ll now discuss some good practices of multiple inheritance and see whether Tkinter goes along with them.

Coping with Multiple Inheritance

[...] we needed a better theory about inheritance entirely (and still do). For example, inheritance and instancing (which is a kind of inheritance) muddles both pragmatics (such as factoring code to save space) and semantics (used for way too many tasks such as: specialization, generalization, speciation, etc.).
— Alan Kay The Early History of Smalltalk

As Alan Kay wrote, inheritance is used for different reasons, and multiple inheritance adds alternatives and complexity. It’s easy to create incomprehensible and brittle designs using multiple inheritance. Because we don’t have a comprehensive theory, here are a few tips to avoid spaghetti class graphs.

1. Distinguish Interface Inheritance from Implementation Inheritance

When dealing with multiple inheritance, it’s useful to keep straight the reasons why subclassing is done in the first place. The main reasons are:

• Inheritance of interface creates a subtype, implying an “is-a” relationship.
• Inheritance of implementation avoids code duplication by reuse.

In practice, both uses are often simultaneous, but whenever you can make the intent clear, do it. Inheritance for code reuse is an implementation detail, and it can often be replaced by composition and delegation. On the other hand, interface inheritance is the backbone of a framework.

2. Make Interfaces Explicit with ABCs

In modern Python, if a class is designed to define an interface, it should be an explicit ABC. In Python ≥ 3.4, this means: subclass abc.ABC or another ABC (see “ABC Syntax Details” on page 328 if you need to support older Python versions).

3. Use Mixins for Code Reuse 使用Mixin实现代码复用

If a class is designed to provide method implementations for reuse by multiple unrelated subclasses, without implying an “is-a” relationship, it should be an explicit mixin class. Conceptually, a mixin does not define a new type; it merely bundles methods for reuse. A mixin should never be instantiated, and concrete classes should not inherit only from a mixin. Each mixin should provide a single specific behavior, implementing few and very closely related methods.

4. Make Mixins Explicit by Naming

There is no formal way in Python to state that a class is a mixin, so it is highly recom‐ mended that they are named with a ...Mixin suffix. Tkinter does not follow this advice, but if it did, XView would be XViewMixin, Pack would be PackMixin, and so on with all the classes where I put the «mixin» tag in Figure 12-3.

5. An ABC May Also Be a Mixin; The Reverse Is Not True

Because an ABC can implement concrete methods, it works as a mixin as well. An ABC also defines a type, which a mixin does not. And an ABC can be the sole base class of any other class, while a mixin should never be subclassed alone except by another, more specialized mixin—not a common arrangement in real code.

One restriction applies to ABCs and not to mixins: the concrete methods implemented in an ABC should only collaborate with methods of the same ABC and its superclasses. This implies that concrete methods in an ABC are always for convenience, because everything they do, a user of the class can also do by calling other methods of the ABC.

6. Don’t Subclass from More Than One Concrete Class 不要子类化一个以上的具象类

Concrete classes should have zero or at most one concrete superclass.[6] In other words, all but one of the superclasses of a concrete class should be ABCs or mixins. For example, in the following code, if Alpha is a concrete class, then Beta and Gamma must be ABCs or mixins:

具象类应当拥有零个或者最多一个具象子类。换言之,

class MyConcreteClass(Alpha, Beta, Gamma):
"""This is a concrete class: it can be instantiated.""" # ... more code ...

7. Provide Aggregate Classes to Users 提供聚合类

If some combination of ABCs or mixins is particularly useful to client code, provide a class that brings them together in a sensible way. Grady Booch calls this an aggregate class.[7]

For example, here is the complete source code for tkinter.Widget:

class Widget(BaseWidget, Pack, Place, Grid):
        """Internal class.
Base class for a widget which can be positioned with the geometry managers Pack, Place or Grid."""
pass

[6]. In “Waterfowl and ABCs” on page 314, Alex Martelli quotes Scott Meyer’s More Effective C++, which goes even further: “all non-leaf classes should be abstract” (i.e., concrete classes should not have concrete super‐ classes at all).

[7]. “A class that is constructed primarily by inheriting from mixins and does not add its own structure or behavior is called an aggregate class.”, Grady Booch et al., Object Oriented Analysis and Design, 3E (Addison-Wesley, 2007), p. 109.

The body of Widget is empty, but the class provides a useful service: it brings together four superclasses so that anyone who needs to create a new widget does not need to remember all those mixins, or wonder if they need to be declared in a certain order in a class statement. A better example of this is the Django ListView class, which we’ll discuss shortly, in “A Modern Example: Mixins in Django Generic Views” on page 362.

8. “Favor Object Composition Over Class Inheritance.”

This quote comes straight the Design Patterns book,(#8) and is the best advice I can offer here. Once you get comfortable with inheritance, it’s too easy to overuse it. Placing objects in a neat hierarchy appeals to our sense of order; programmers do it just for fun.

#8. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Introduction, p. 20.

However, favoring composition leads to more flexible designs. For example, in the case of the tkinter.Widget class, instead of inheriting the methods from all geometry man‐ agers, widget instances could hold a reference to a geometry manager, and invoke its methods. After all, a Widget should not “be” a geometry manager, but could use the services of one via delegation. Then you could add a new geometry manager without touching the widget class hierarchy and without worrying about name clashes. Even with single inheritance, this principle enhances flexibility, because subclassing is a form of tight coupling, and tall inheritance trees tend to be brittle.

Composition and delegation can replace the use of mixins to make behaviors available to different classes, but cannot replace the use of interface inheritance to define a hier‐ archy of types.

We will now analyze Tkinter from the point of view of these recommendations.

Tkinter: The Good, the Bad, and the Ugly

Keep in mind that Tkinter has been part of the standard library since Python 1.1 was released in 1994. Tkinter is a layer on top of the excellent Tk GUI toolkit of the Tcl language. The Tcl/Tk combo is not originally object oriented, so the Tk API is basical‐ ly a vast catalog of functions. However, the toolkit is very object oriented in its concepts, if not in its implementation.

Most advice in the previous section is not followed by Tkinter, with #7 being a notable exception. Even then, it’s not a great example, because composition would probably work better for integrating the geometry managers into Widget, as discussed in #8.

The docstring of tkinter.Widget starts with the words “Internal class.” This suggests that Widget should probably be an ABC. Although Widget has no methods of its own, it does define an interface. Its message is: “You can count on every Tkinter widget pro‐ viding basic widget methods (init, destroy, and dozens of Tk API functions), in addition to the methods of all three geometry managers.” We can agree that this is not a great interface definition (it’s just too broad), but it is an interface, and Widget “defines” it as the union of the interfaces of its superclasses.

The Tk class, which encapsulates the GUI application logic, inherits from Wm and Misc, neither of which are abstract or mixin (Wm is not proper mixin because TopLevel sub‐ classes only from it). The name of the Misc class is—by itself—a very strong code smell. Misc has more than 100 methods, and all widgets inherit from it. Why is it nec‐ essary that every single widget has methods for clipboard handling, text selection, timer management, and the like? You can’t really paste into a button or select text from a scrollbar. Misc should be split into several specialized mixin classes, and not all widgets should inherit from every one of those mixins.

To be fair, as a Tkinter user, you don’t need to know or use multiple inheritance at all. It’s an implementation detail hidden behind the widget classes that you will instantiate or subclass in your own code. But you will suffer the consequences of excessive multiple inheritance when you type dir(tkinter.Button) and try to find the method you need among the 214 attributes listed.

Despite the problems, Tkinter is stable, flexible, and not necessarily ugly. The legacy (and default) Tk widgets are not themed to match modern user interfaces, but the tkinter.ttk package provides pretty, native-looking widgets, making professional GUI development viable since Python 3.1 (2009). Also, some of the legacy widgets, like Canvas and Text, are incredibly powerful. With just a little coding, you can turn a Canvas object into a simple drag-and-drop drawing application. Tkinter and Tcl/Tk are defi‐ nitely worth a look if you are interested in GUI programming.

However, our theme here is not GUI programming, but the practice of multiple inher‐ itance. A more up-to-date example with explicit mixin classes can be found in Django.

A Modern Example: Mixins in Django Generic Views

You don’t need to know Django to follow this section. I am just using a small part of the framework as a practical example of multiple inheritance, and I will try to give all the necessary back‐ ground, assuming you have some experience with server-side web development in another language or framework.

In Django, a view is a callable object that takes, as argument, an object representing an HTTP request and returns an object representing an HTTP response. The different responses are what interests us in this discussion. They can be as simple as a redirect response, with no content body, or as complex as a catalog page in an online store, rendered from an HTML template and listing multiple merchandise with buttons for buying and links to detail pages.

Originally, Django provided a set of functions, called generic views, that implemented some common use cases. For example, many sites need to show search results that include information from numerous items, with the listing spanning multiple pages, and for each item a link to a page with detailed information about it. In Django, a list view and a detail view are designed to work together to solve this problem: a list view renders search results, and a detail view produces pages for individual items.

However, the original generic views were functions, so they were not extensible. If you needed to do something similar but not exactly like a generic list view, you’d have to start from scratch.

In Django 1.3, the concept of class-based views was introduced, along with a set of generic view classes organized as base classes, mixins, and ready-to-use concrete classes. The base classes and mixins are in the base module of the django.views.generic package, pictured in Figure 12-4. At the top of the diagram we see two classes that take care of very distinct responsibilities: View and TemplateResponseMixin.

A great resource to study these classes is the Classy Class-Based Views website, where you can easily navigate through them, see all methods in each class (inherited, overridden, and added meth‐ ods), view diagrams, browse their documentation, and jump to their source code on GitHub.

View is the base class of all views (it could be an ABC), and it provides core functionality like the dispatch method, which delegates to “handler” methods like get, head, post, etc., implemented by concrete subclasses to handle the different HTTP verbs.(#9) The RedirectView class inherits only from View, and you can see that it implements get, head, post, etc.

#9. Django programmers know that the as_view class method is the most visible part of the View interface, but it’s not relevant to us here.

Concrete subclasses of View are supposed to implement the handler methods, so why aren’t they part of the View interface? The reason: subclasses are free to implement just the handlers they want to support. A TemplateView is used only to display content, so it only implements get. If an HTTP POST request is sent to a TemplateView, the inherited View.dispatch method checks that there is no post handler, and produces an HTTP 405 Method Not Allowed response.(#10)

#10. If you are into design patterns, you’ll notice that the Django dispatch mechanism is a dynamic variation of the Template Method pattern. It’s dynamic because the View class does not force subclasses to implement all handlers, but dispatch checks at runtime if a concrete handler is available for the specific request.

image:bypass

Figure 12-4. UML class diagram for the django.views.generic.base module

The TemplateResponseMixin provides functionality that is of interest only to views that need to use a template. A RedirectView, for example, has no content body, so it has no need of a template and it does not inherit from this mixin. TemplateResponseMixin provides behaviors to TemplateView and other template-rendering views, such as List View, DetailView, etc., defined in other modules of the django.views.generic package. Figure 12-5 depicts the django.views.generic.list module and part of the base module.

iamge:pass

Figure 12-5. UML class diagram for the django.views.generic.list module. Here the three classes of the base module are collapsed (see Figure 12-4). The ListView class has no methods or attributes: it’s an aggregate class.

For Django users, the most important class in Figure 12-5 is ListView, which is an aggregate class, with no code at all (its body is just a docstring). When instantiated, a ListView has an object_list instance attribute through which the template can iterate to show the page contents, usually the result of a database query returning multiple objects. All the functionality related to generating this iterable of objects comes from the MultipleObjectMixin. That mixin also provides the complex pagination logic—to display part of the results in one page and links to more pages.

Suppose you want to create a view that will not render a template, but will produce a list of objects in JSON format. Thats’ why the BaseListView exists. It provides an easy- to-use extension point that brings together View and MultipleObjectMixin function‐ ality, without the overhead of the template machinery.

The Django class-based views API is a better example of multiple inheritance than Tkinter. In particular, it is easy to make sense of its mixin classes: each has a well-defined purpose, and they are all named with the ...Mixin suffix.

Class-based views were not universally embraced by Django users. Many do use them in a limited way, as black boxes, but when it’s necessary to create something new, a lot of Django coders continue writing monolithic view functions that take care of all those responsibilities, instead of trying to reuse the base views and mixins.

It does take some time to learn how to leverage class-based views and how to extend them to fulfill specific application needs, but I found that it was worthwhile to study them: they eliminate a lot of boilerplate code, make it easier to reuse solutions, and even improve team communication—for example, by defining standard names to templates, and to the variables passed to template contexts. Class-based views are Django views “on rails.”

This concludes our tour of multiple inheritance and mixin classes.

Chapter Summary 本章总结

We started our coverage of inheritance explaining the problem with subclassing built- in types: their native methods implemented in C do not call overridden methods in subclasses, except in very few special cases. That’s why, when we need a custom list, dict, or str type, it’s easier to subclass UserList, UserDict, or UserString—all defined in the collections module, which actually wraps the built-in types and delegate op‐ erations to them—three examples of favoring composition over inheritance in the stan‐ dard library. If the desired behavior is very different from what the built-ins offer, it may be easier to subclass the appropriate ABC from collections.abc and write your own implementation.

我们从使用子类化内建类型来解释继承的问题:这些原生方法使用C实现,

The rest of the chapter was devoted to the double-edged sword of multiple inheritance. First we saw how the method resolution order, encoded in the mro class attribute, addresses the problem of potential naming conflicts in inherited methods. We also saw how the super() built-in follows the mro to call a method on a superclass. We then studied how multiple inheritance is used in the Tkinter GUI toolkit that comes with the Python standard library. Tkinter is not an example of current best practices, so we discussed some ways of coping with multiple inheritance, including careful use of mixin classes and avoiding multiple inheritance altogether by using composition instead. After considering how multiple inheritance is abused in Tkinter, we wrapped up by studying the core parts of the Django class-based views hierarchy, which I consider a better ex‐ ample of mixin usage. Lennart Regebro—a very experienced Pythonista and one of this book’s technical re‐ viewers—finds the design of Django’s mixin views hierarchy confusing. But he also wrote:

章节余下的内容用来专门讨论多重继承的双刃剑问题。首先,我们看到了方法解析顺序,如何被编码在类的__mro__属性中,解

   The dangers and badness of multiple inheritance are greatly overblown. I’ve actually never had a    real big problem with it.

In the end, each of us may have different opinions about how to use multiple inheritance, or whether to use it at all in our own projects. But often we don’t have a choice: the frameworks we must use impose their own choices.

最后,我们每个人对于如何使用多重继承都有自己的不同观点,抑或是无视情况都将它用在自己的项目中。然而有时候我们也是没得选择:

Further Reading 深入阅读

When using ABCs, multiple inheritance is not only common but practically inevitable, because each of the most fundamental collection ABCs (Sequence, Mapping, and Set) extend multiple ABCs. The source code for collections.abc (Lib/_collections_abc.py) is a good example of multiple inheritance with ABCs—many of which are also mixin classes.

在使用ABC时,多重继承并不常见但实践不可避免,因为

Raymond Hettinger’s post Python’s super() considered super! explains the workings of super and multiple inheritance in Python from a positive perspective. It was written in response to Python’s Super is nifty, but you can’t use it (a.k.a. Python’s Super Considered Harmful) by James Knight.

Raymond Hettinger的文章《Python的super()过人之处!》从一个正面的视角解释了super的工作原理和在Pythno中的多重继承。写就该文以回应James Knight的文章《Python的super非常好,但你却无法使用它》(也被称为《Python中super的坏处》)。

Despite the titles of those posts, the problem is not really the super built-in—which in Python 3 is not as ugly as it was in Python 2. The real issue is multiple inheritance, which is inherently complicated and tricky. Michele Simionato goes beyond criticizing and actually offers a solution in his Setting Multiple Inheritance Straight: he implements traits, a constrained form of mixins that originated in the Self language. Simionato has a long series of illuminating blog posts about multiple inheritance in Python, including The wonders of cooperative inheritance, or using super in Python 3; Mixins considered harmful, part 1 and part 2; and Things to Know About Python Super, part 1, part 2 and part 3. The oldest posts use the Python 2 super syntax, but are still relevant.

抛开这些文章的标题,内建super的问题不在于Python3中它比Python2中好看多少。真正问题在于多重继承,它在继承上面的复杂和奇技淫巧。Michele Simionato

I read the first edition of Grady Booch’s Object Oriented Analysis and Design, 3E (Addison-Wesley, 2007), and highly recommend it as a general primer on object ori‐ ented thinking, independent of programming language. It is a rare book that covers multiple inheritance without prejudice.

我读过Gray Booch的第一版面向对象分析和设计,以及第三版(由Addison-Wesley出版社2007出版),

####Soapbox #####Think About the Classes You Really Need The vast majority of programmers write applications, not frameworks. Even those who do write frameworks are likely to spend a lot (if not most) of their time writing appli‐ cations. When we write applications, we normally don’t need to code class hierarchies. At most, we write classes that subclass from ABCs or other classes provided by the framework. As application developers, it’s very rare that we need to write a class that will act as the superclass of another. The classes we code are almost always leaf classes (i.e., leaves of the inheritance tree).

If, while working as an application developer, you find yourself building multilevel class hierarchies, it’s likely that one or more of the following applies:

• You are reinventing the wheel. Go look for a framework or library that provides components you can reuse in your application.

• You are using a badly designed framework. Go look for an alternative.

• You are overengineering. Remember the KISS principle.

• You became bored coding applications and decided to start a new framework. Congratulations and good luck! It’s also possible that all of the above apply to your situation: you became bored and decided to reinvent the wheel by building your own overengineered and badly designed framework, which is forcing you to code class after class to solve trivial problems. Hopefully you are having fun, or at least getting paid for it.

Misbehaving Built-ins: Bug or Feature?

The built-in dict, list, and str types are essential building blocks of Python itself, so they must be fast—any performance issues in them would severely impact pretty much everything else. That’s why CPython adopted the shortcuts that cause their built-in methods to misbehave by not cooperating with methods overridden by subclasses. A possible way out of this dilemma would be to offer two implementations for each of those types: one “internal,” optimized for use by the interpreter and an external, easily extensible one.

But wait, this is what we have: UserDict, UserList, and UserString are not as fast as the built-ins but are easily extensible. The pragmatic approach taken by CPython means we also get to use, in our own applications, the highly optimized implementations that are hard to subclass. Which makes sense, considering that it’s not so often that we need a custom mapping, list, or string, but we use dict, list and str every day. We just need to be aware of the trade-offs involved.

Inheritance Across Languages

Alan Kay coined the term “object oriented,” and Smalltalk had only single inheritance, although there are forks with various forms of multiple inheritance support, including the modern Squeak and Pharo Smalltalk dialects that support traits—a language con‐ struct that fulfills the role of a mixin class, while avoiding some of the issues with multiple inheritance.

The first popular language to implement multiple inheritance was C++, and the feature was abused enough that Java—intended as a C++ replacement—was designed without support for multiple inheritance of implementation (i.e., no mixin classes). That is, until Java 8 introduced default methods that make interfaces very similar to the abstract classes used to define interfaces in C++ and in Python. Except that Java interfaces cannot have state—a key distinction. After Java, probably the most widely deployed JVM lan‐ guage is Scala, and it implements traits. Other languages supporting traits are the latest stable versions of PHP and Groovy, and the under-construction languages Rust and Perl 6—so it’s fair to say that traits are trendy as I write this.

Ruby offers an original take on multiple inheritance: it does not support it, but intro‐ duces mixins as a language feature. A Ruby class can include a module in its body, so the methods defined in the module become part of the class implementation. This is a “pure” form of mixin, with no inheritance involved, and it’s clear that a Ruby mixin has no influence on the type of the class where it’s used. This provides the benefits of mixins, while avoiding many of its usual problems.

Two recent languages that are getting a lot of traction severely limit inheritance: Go and Julia. Go has no inheritance at all, but it implements interfaces in a way that resembles a static form of duck typing (see “Soapbox” on page 343 for more about this). Julia avoids the terms “classes” and has only “types.” Julia has a type hierarchy but subtypes cannot inherit structure, only behaviors, and only abstract types can be subtyped. In addition, Julia methods are implemented using multiple dispatch—a more advanced form of the mechanism we saw in “Generic Functions with Single Dispatch” on page 202.



演讲台