《流畅的Python》
http://shop.oreilly.com/product/0636920032519.do
《Fluent Python 副标题 Clear, Concise, and Effective Programming》
Learn how to write idiomatic, effective Python code by leveraging its best features. Python's simplicity quickly lets you become productive with it, but this often means you aren’t using everything the language has to offer. By taking you through Python’s key language features and libraries, this practical book shows you how to make your code shorter, faster, and more readable all at the same time—what experts consider Pythonic.
Many programmers who learn Python basics fall into the trap of reinventing the wheel because of past experience in other languages, and try to bend the language to patterns that don't really apply to it. Author Luciano Ramalho, a Python Software Foundation member and Python programmer for 15 years, helps you drop your accent from another language so you can code Python fluently.
Learn practical applications of generators for database processing Rethink some design patterns in a Python context Examine attribute descriptors and when to use them: the key to ORMs Explore Pythonic objects: protocols versus interfaces, abstract base classes and multiple inheritance
Luciano Ramalho
O'Reilly Media
August 2015 (美国东部时间.)
750
预览
第十二章 继承该如何是好
[我们]开始去推动继承**,使其成为了新手也可以构建以前只有原专家才可以设计的框架。
— 阿兰.凯《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:dict
的update
方法也没有使用我们定义的__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
模块的UserDict
,UserList
和UserString
派生类,而不是子类化内建。
如果你子类化collections.UserDict
而不是dict
,那么例子12-1和例子12-2中的问题都会被该解决。见例子12-3。
例子12-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-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)
注意类B
和C
都实现了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)。