We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
第十二章 继承该如何是好
[我们]开始去推动继承思想,使其成为了新手也可以构建以前只有原专家才可以设计的框架。 — 阿兰.凯《Smalltalk的早期历史》
[我们]开始去推动继承思想,使其成为了新手也可以构建以前只有原专家才可以设计的框架。
— 阿兰.凯《Smalltalk的早期历史》
本章涉及到了继承和子类化,这里有两处特别强调的针对Python的细节:
很多人认为多重继承带来的麻烦远大于其带来的好处。
然而,由于Java特别出色并具有广泛的影响力,这就意味着,在实际编程活动中很多程序员并没有见过多重继承。这就是为什么我们通过两个重要的项目来阐明多重继承的适应范围:Tkinter GUI套件,以及Django web 框架的原因。
Tkinter GUI
我们从子类化内建类型的问题开始。余下的章节会用案例研究并学习多重继承,讨论在构建类的分层设计时会遇到的问题。
在Python2.2之前,子类化list或者dict这样的内建类型是不可能的。打那以后,Python虽然可以做到子类化内建类型,但是仍然要面对的重要警告是:内建的代码(由C语言重写)并不会调用被通过用户自定义类所覆盖的特殊方法。
list
dict
对问题的准确描述都放在了PyPy文档,以及内建类型的子类化一节中的PyPy和CPython之间差异:
PyPy
PyPy和CPython之间差异
正式地来说,Cpython对完全地重写内建类型的子类方法时是否明确地调用毫无规则可循。大略上,这些方法从来没有被其他的相同对象的内建方法所调用。例如,dict子类中的重写__getitem__()不会被get()这样的内建方法调用。
__getitem__()
get()
例子12-1阐明了此问题。
例子12-1。重写的__setitem__被dict的__init__和__update__方法所忽略。
__setitem__
__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__会使值重复(由于这个不好原因,因此必须有可见的效果)。它在委托到超类时才会正常运行。
DoppelDict.__setitem__
2:继承自dict的__init__方法,明确地忽略了重写的__setitem__:'one'的值并没有重复。
'one'
3:[]运算符调用__setitem__,并如所希望的那样运行:'two'映射到了重复的值[2, 2]。
[]
'two'
[2, 2]
4:dict的update方法也没有使用我们定义的__setitem__:值'three'没有被重复。
update
'three'
该内建行为违反了面向对象的基本准则:方法的搜索应该总是从目标实例(self)的类开始,甚至是调用发生在以超类实现的方法之内部。在这样的悲观的情形下,
self
问题是在一个实例内部没有调用的限制,例如,不论self.get()是否调用self.__getitem__(),都会出现会被内建方法所调用其他类的方法被重写。下面是改编自PyPy文档的例子:
self.get()
self.__getitem__()
PyPy文档
例子12-2。AnswerDict的__getitem__被dict.update所忽略。
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,不论键是什么。
AnserDict.__getitem__
42
2:ad是一个带有键值对('a', 'foo')的AnswerDict。
ad
('a', 'foo')
3:ad['a']如所期望的那样返回42。
ad['a']
4:d是一个普通使用ad更新的dict实例。
d
5:dict.update方法忽略了AnserDict.__getitem__。
警告 直接地子类化类似dict或者list或者str这样的内建类型非常容易出错,因为大多数的内建方法会忽略用户所定义的重写方法。从被设计成易于扩展的collections模块的UserDict,UserList和UserString派生类,而不是子类化内建。
直接地子类化类似dict或者list或者str这样的内建类型非常容易出错,因为大多数的内建方法会忽略用户所定义的重写方法。从被设计成易于扩展的collections模块的UserDict,UserList和UserString派生类,而不是子类化内建。
str
collections
UserDict
UserList
UserString
如果你子类化collections.UserDict而不是dict,那么例子12-1和例子12-2中的问题都会被该解决。见例子12-3。
collections.UserDict
例子12-3。DoppelDict2和AnswerDict2一如所希望的运行,因为它们扩展的是UserDict而不是dict。
DoppelDict2
AnswerDict2
>>> 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__。
StrKeyDict
__missing__
___contains__
总结:本节所描述的问题仅应用于在C语言内的方法委托实现内建类型,而且仅对用户定义的派生自这些的类型的类有效果。如果你在Python中子类化类编程,比如,UserDict或者MutableMapping,你不会遇到麻烦的。
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。
B
C
pong
C.pong
PONG
如果你对实例D调用d.pong(),实际上哪一个pong方法会运行呢?对于C++程序员来说他们必须具有使用类名称调用方法,以解决这个模棱两可的问题。这样的问题在Python中也能够解决。看下例子12-5就知道了。
D
d.pong()
例子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
像d.pong()这样的模棱两可的调用得以解决,因为Python在穿越继承图时,遵循一个特定的顺序。这个顺序就叫做MRO:方法解析顺序。类有一个被称为__mro__的属性,它拥有使用MRO顺序的超类的引用元组,即,当前的类的所有到object类的路径。拿类D来说明什么是__mro__(参见 图表12-1):
__mro__
object
>>> 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方法可以这样写:
super()
pingpong
D.ping
def ping(self): A.ping(self) # instead of super().ping() print('post-ping:', self)
注意,当调用直接调用一个类的实例时,你必须明确地传递self,因为你访问的是unbound method。
unbound method
不过,这是最安全的而且更未来化的使用super(),特别是在调用一个框架的方法时,或者任何不受你控制的类继承时。例子12-6演示了在调用方法时super()对MRO的遵循。
例子12-6。使用super()去调用ping(源码见例子12-4)。
ping
The text was updated successfully, but these errors were encountered:
其他的章节 怎么没有啊?
Sorry, something went wrong.
@istommao 业余时间不够用?
No branches or pull requests
第十二章 继承该如何是好
本章涉及到了继承和子类化,这里有两处特别强调的针对Python的细节:
很多人认为多重继承带来的麻烦远大于其带来的好处。
然而,由于Java特别出色并具有广泛的影响力,这就意味着,在实际编程活动中很多程序员并没有见过多重继承。这就是为什么我们通过两个重要的项目来阐明多重继承的适应范围:
Tkinter GUI
套件,以及Django web 框架的原因。我们从子类化内建类型的问题开始。余下的章节会用案例研究并学习多重继承,讨论在构建类的分层设计时会遇到的问题。
子类化内建类型是需要些技巧的
在Python2.2之前,子类化
list
或者dict
这样的内建类型是不可能的。打那以后,Python虽然可以做到子类化内建类型,但是仍然要面对的重要警告是:内建的代码(由C语言重写)并不会调用被通过用户自定义类所覆盖的特殊方法。对问题的准确描述都放在了
PyPy
文档,以及内建类型的子类化一节中的PyPy和CPython之间差异
:例子12-1阐明了此问题。
例子12-1。重写的
__setitem__
被dict
的__init__
和__update__
方法所忽略。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
所忽略。1:
AnserDict.__getitem__
总是返回42
,不论键是什么。2:
ad
是一个带有键值对('a', 'foo')
的AnswerDict
。3:
ad['a']
如所期望的那样返回42。4:
d
是一个普通使用ad
更新的dict
实例。5:
dict.update
方法忽略了AnserDict.__getitem__
。如果你子类化
collections.UserDict
而不是dict
,那么例子12-1和例子12-2中的问题都会被该解决。见例子12-3。例子12-3。
DoppelDict2
和AnswerDict2
一如所希望的运行,因为它们扩展的是UserDict而不是dict。为了估量内建的子类工作所要求体验,我重写了例子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中的图。
注意类
B
和C
都实现了pong
方法。唯一的不同是C.pong
输出大写的单词PONG
。如果你对实例
D
调用d.pong()
,实际上哪一个pong
方法会运行呢?对于C++程序员来说他们必须具有使用类名称调用方法,以解决这个模棱两可的问题。这样的问题在Python中也能够解决。看下例子12-5就知道了。例子12-5.对类D的实例的pong方法调用的两种形式。
1: 简单地调用
d.pong
导致B的运行。2: 你可以总是直接地对调用超类的方法,传递实例作为明确的参数。
像
d.pong()
这样的模棱两可的调用得以解决,因为Python在穿越继承图时,遵循一个特定的顺序。这个顺序就叫做MRO:方法解析顺序。类有一个被称为__mro__
的属性,它拥有使用MRO顺序的超类的引用元组,即,当前的类的所有到object
类的路径。拿类D
来说明什么是__mro__
(参见 图表12-1):推荐的调用超类的委托方法就是内建的
super()
函数,这样做是因为在Python3中较易使用,就像例子12-4中的类D的pingpong
方法所阐述的那样。不过,有时候忽略MRO,对超类直接地调用方法也是也可以的,而且很方便。例如,D.ping
方法可以这样写:注意,当调用直接调用一个类的实例时,你必须明确地传递
self
,因为你访问的是unbound method
。不过,这是最安全的而且更未来化的使用
super()
,特别是在调用一个框架的方法时,或者任何不受你控制的类继承时。例子12-6演示了在调用方法时super()
对MRO的遵循。例子12-6。使用
super()
去调用ping
(源码见例子12-4)。真实世界中的多重继承
应对多重继承
1. 接口继承和接口实现之间的区别
继承
2. 使用ABC让接口更清晰
3. 为了代码重复利用而使用mixin
The text was updated successfully, but these errors were encountered: