>>> 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
{'one': 1, 'two': [2, 2], 'three': 3}❶ DoppelDict.__setitem__方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。
❷ 继承自dict的__init__方法显然忽略了我们覆盖的__setitem__方法:'one'的值没有重复。
❸ []运算符会调用我们覆盖的__setitem__方法,按预期那样工作:'two'对应的是两个重复的值,即[2, 2]。
❹ 继承自dict的update方法也不使用我们覆盖的__setitem__方法:'three'的值没有重复。
>>> 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']
'foo'
>>> d
{'a': 'foo'}❶ DoppelDict.__setitem__方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。
❷ 继承自dict的__init__方法显然忽略了我们覆盖的__setitem__方法:'one'的值没有重复。
❸ []运算符会调用我们覆盖的__setitem__方法,按预期那样工作:'two'对应的是两个重复的值,即[2, 2]。
❹ 继承自dict的update方法也不使用我们覆盖的__setitem__方法:'three'的值没有重复。
直接子类化内置类型(如dict、list或str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承collections模块中的类,例如UserDict、UserList和UserString,这些类做了特殊设计,因此易于扩展。
>>> 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}使用collections.UserDict直接解决上面1,2问题。
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”。
class A:
def ping(self):
print("A_ping: ", self)
class B(A):
def pong(self):
print("B_pong: ", self)
class C(A):
def pong(self):
print("C_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)>>> from diamond import *
>>> d = D()
>>> d.pong() #1
B_pong: <diamond.D object at 0x7f386ed7c358>
>>> C.pong(d) #2
C_pong: <diamond.D object at 0x7f386ed7c358>
>>> d.ping() #1
A_ping: <diamond.D object at 0x7f386ed7c358> #2
post-ping: <diamond.D object at 0x7f386ed7c358> #3❶ 直接调用d.pong( )运行的是B类中的版本。
❷ 超类中的方法都可以直接调用,此时要把实例作为显式参数传入。
❶ D类的ping方法做了两次调用。
❷ 第一个调用是super( ).ping( );super函数把ping调用委托给A类;这一行由A.ping输出。
❸ 第二个调用是print('post-ping:', self),输出的是这一行。
Python能区分d.pong( )调用的是哪个方法,是因为Python会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为__mro__的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object类。D类的__mro__属性如下
>>> D.__mro__
(<class 'diamond.D'>, <class 'diamond.B'>, <class 'diamond.C'>, <class 'diamond.A'>, <class 'object'>)
若想把方法调用委托给超类,推荐的方式是使用内置的super( )函数。在Python 3中,这种方式变得更容易了,如示例D类的pingpong方法所示。[插图]然而,有时可能需要绕过方法解析顺序,直接调用某个超类的方法——这样做有时更方便。
>>> d.pingpong()
A_ping: <diamond.D object at 0x7f386ed7c358> #1
post-ping: <diamond.D object at 0x7f386ed7c358>
A_ping: <diamond.D object at 0x7f386ed7c358> #2
B_pong: <diamond.D object at 0x7f386ed7c358> #3
B_pong: <diamond.D object at 0x7f386ed7c358> #4
C_pong: <diamond.D object at 0x7f386ed7c358> #5❶ 第一个调用是self.ping( ),运行的是D类的ping方法,输出这一行和下一行。
❷ 第二个调用是super( ).ping( ),跳过D类的ping方法,找到A类的ping方法。
❸ 第三个调用是self.pong( ),根据__mro__ ,找到的是B类实现的pong方法。
❹ 第四个调用是super( ).pong( ),也根据__mro__ ,找到B类实现的pong方法。
➎第五个调用是C.pong(self),忽略mro ,找到的是C类实现的pong方法。
方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果在diamond.py文件中把D类声明为class D(C, B):,那么D类的__mro__属性就会不一样:先搜索C类,再搜索B类。
把接口继承和实现继承区分开
使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有:
继承接口,创建子类型,实现“是什么”关系
继承实现,通过重用避免代码重复
其实这两条经常同时出现,不过只要可能,一定要明确意图。通过继承重用代码是实现细节,通常可以换用组合和委托模式。而接口继承则是框架的支柱。
使用抽象基类显式表示接口
现代的Python中,如果类的作用是定义接口,应该明确把它定义为抽象基类。Python 3.4及以上的版本中,我们要创建abc.ABC或其他抽象基类的子类。
通过混入重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。
在名称中明确指明混入
因为在Python中没有把类声明为混入的正规方式,所以强烈推荐在名称中加入...Mixin后缀。
抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,因此也可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。此外,抽象基类可以作为其他类的唯一基类,而混入决不能作为唯一的超类,除非继承另一个更具体的混入——真实的代码很少这样做。
抽象基类有个局限是混入没有的:抽象基类中实现的具体方法只能与抽象基类及其超类中的方法协作。这表明,抽象基类中的具体方法只是一种便利措施,因为这些方法所做的一切,用户调用抽象基类中的其他方法也能做到。
不要子类化多个具体类
具体类可以没有,或最多只有一个具体超类。[插图]也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。
为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们结合起来。
__mro__类属性中蕴藏的方法解析顺序,有了这一机制,继承方法的名称不再会发生冲突。我们还提到,内置的super( )函数会按照__mro__属性给出的顺序调用超类的方法。
大多数程序员编写应用程序而不开发框架。即便是开发框架的那些人,多数时候(或大多数时候)也是在编写应用程序。编写应用程序时,我们通常不用设计类的层次结构。我们至多会编写子类、继承抽象基类或框架提供的其他类。作为应用程序开发者,我们极少需要编写作为其他类的超类的类。我们自己编写的类几乎都是末端类(即继承树的叶子)。
在某些圈子中,运算符重载的名声并不好。这个语言特性可能(已经)被滥用,让程序员困惑,导致缺陷和意料之外的性能瓶颈。但是,如果使用得当,API会变得好用,代码会变得易于阅读。Python施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:
不能重载内置类型的运算符
不能新建运算符,只能重载现有的
某些运算符不能重载——is、and、or和not(不过位运算符&、|和~可以)
-(__neg__)
一元取负算术运算符。如果x是-2,那么-x==2。
+(__pos__)
一元取正算术运算符。通常,x==+x,但也有一些例外。如果好奇,请阅读“x和+x何时不相等”附注栏。
~(__invert__)
对整数按位取反,定义为x==-(x+1)。如果x是2,那么x==-3。
支持一元运算符很简单,只需实现相应的特殊方法。这些特殊方法只有一个参数,self。然后,使用符合所在类的逻辑实现。不过,要遵守运算符的一个基本规则:始终返回一个新对象。也就是说,不能修改self,要创建并返回合适类型的新实例。
算术运算上下文的精度变化可能导致x不等于+x
>>> import decimal
>>> ctx = decimal.getcontext() #1
>>> ctx.prec = 40 #2
>>> one_third = decimal.Decimal('1')/decimal.Decimal('3') #3
>>> one_third #4
Decimal('0.3333333333333333333333333333333333333333')
>>> one_third == +one_third #5
True
>>> ctx.prec = 28 #6
>>> one_third == +one_third #7
False
>>> one_third
Decimal('0.3333333333333333333333333333333333333333')
>>> +one_third #8
Decimal('0.3333333333333333333333333333')❶ 获取当前全局算术运算的上下文引用。
❷ 把算术运算上下文的精度设为40。
❸ 使用当前精度计算1/3。
❹ 查看结果,小数点后有40个数字。
❺ one_third==+one_third返回True。
❻ 把精度降低为28,这是Python 3.4为Decimal算术运算设定的默认精度。
❼ 现在,one_third==+one_third返回False。
❽ 查看+one_third,小数点后有28个数字。
虽然每个+one_third表达式都会使用one_third的值创建一个新Decimal实例,但是会使用当前算术运算上下文的精度。
涉及不同类型的运算,Python为中缀运算符特殊方法提供了特殊的分派机制。对表达式a+b来说,解释器会执行以下几步操作
(1)如果a有__add__方法,而且返回值不是NotImplemented,调用a.__add__(b),然后返回结果。
(2)如果a没有__add__方法,或者调用__add__方法返回NotImplemented,检查b有没有__radd__方法,如果有,而且没有返回NotImplemented,调用b._radd_(a),然后返回结果。
(3)如果b没有__radd__方法,或者调用__radd__方法返回NotImplemented,抛出TypeError,并在错误消息中指明操作数类型不支持。
__radd__是__add__的“反射”(reflected)版本或“反向”(reversed)版本。我喜欢把它叫作“反向”特殊方法。称之为“右向”(right)特殊方法,因为他们在右操作数上调用。不管你喜欢哪个以“r”开头的单词, __radd__和__rsub__等类似方法中的“r”就是这个意思。
这是一种后备机制,如果左操作数没有实现__add__方法,或者实现了,但是返回NotImplemented表明它不知道如何处理右操作数,那么Python会调用__radd__方法。
别把NotImplemented和NotImplementedError搞混了。前者是特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回(return)给解释器。而NotImplementedError是一种异常,抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖。
__radd__通常就这么简单:直接调用适当的运算符,在这里就是委托__add__。任何可交换的运算符都能这么做。处理数字和向量时,+可以交换,但是拼接序列时不行。
在Python中,所有集合都可以迭代。
在Python语言内部,迭代器用于支持:
for循环
构建和扩展集合类型
逐行遍历文本文件
列表推导、字典推导和集合推导
元组拆包
调用函数时,使用*拆包实参
解释器需要迭代对象x时,会自动调用iter(x)。内置的iter函数有以下作用。
(1)检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器。
(2)如果没有实现__iter__方法,但是实现了__getitem__方法,Python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素。
(3)如果尝试失败,Python抛出TypeError异常,通常会提示“C object is not iterable”(C对象不可迭代),其中C是目标对象所属的类。
从Python 3.4开始,检查对象x能否迭代,最准确的方法是:调用iter(x)函数,如果不可迭代,再处理TypeError异常。这比使用isinstance(x, abc.Iterable)更准确,因为iter(x)函数会考虑到遗留的__getitem__方法,而abc.Iterable类则不考虑。
第一版 序列
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __getitem__(self, index):
return self.words[index]
def __len__(self):
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)使用如下:
>>> from sentence import Sentence
>>> s = Sentence('"The time has come," the Walrus said,')
>>> s
Sentence('"The time ha... Walrus said,')
>>> for word in s:
... print(word)
...
The
time
has
come
the
Walrus
said
>>> list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
>>> s[0]
'The'
>>> s[5]
'Walrus'
>>> s[-1]
'said'使用iter内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代;实现了__getitem__方法,而且其参数是从零开始的索引,这种对象也可以迭代。
我们要明确可迭代的对象和迭代器之间的关系:Python从可迭代的对象中获取迭代器。
StopIteration异常表明迭代器到头了。Python语言内部会处理for循环和其他迭代上下文(如列表推导、元组拆包,等等)中的StopIteration异常。
标准的迭代器接口有两个方法。
__next__
返回下一个可用的元素,如果没有元素了,抛出StopIteration异常。
__iter__
返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。
因为迭代器只需__next__和__iter__两个方法,所以除了调用next( )方法,以及捕获StopIteration异常之外,没有办法检查是否还有遗留的元素。此外,也没有办法“还原”迭代器。如果想再次迭代,那就要调用iter(...),传入之前构建迭代器的可迭代对象。传入迭代器本身没用,因为前面说过Iterator.__iter__方法的实现方式是返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器。
迭代器
迭代器是这样的对象:实现了无参数的__next__方法,返回序列中的下一个元素;如果没有元素了,那么抛出StopIteration异常。Python中的迭代器还实现了__iter__方法,因此迭代器也可以迭代。
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个__iter__方法,每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回迭代器本身。
迭代器模式可用来:
访问一个聚合对象的内容而无需暴露它的内部表示
支持对聚合对象的多种遍历
为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)
可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__方法,但不能实现__next__方法。
另一方面,迭代器应该一直可以迭代。迭代器的__iter__方法应该返回自身。
第二版 使用迭代器模式
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self使用如下:
>>> from sentence_iter import Sentence
>>> s = Sentence('"The time has come," the Walrus said,')
>>> s
Sentence('"The time ha... Walrus said,')
>>> for word in s:
... print(word)
...
The
time
has
come
the
Walrus
said
>>> list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']只要Python函数的定义体中有yield关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂。
生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给next(...)函数时,生成器函数会向前,执行函数定义体中的下一个yield语句,返回产出的值,并在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出StopIteration异常——这一点与迭代器协议一致。
第三版 生成器实现
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for word in self.words:
yield word
returnre.finditer函数是re.findall函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject实例。如果有很多匹配,re.finditer函数能节省大量内存。
第四版 惰性实现
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text):
yield match.group()第五版 生成器表达式
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利。
生成器表达式是创建生成器的简洁句法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用。
如果生成器表达式要分成多行写,我倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。
如果函数或构造方法只有一个参数,传入生成器表达式时不用写一对调用函数的括号,再写一对括号围住生成器表达式,只写一对括号就行了。然而,如果生成器表达式后面还有其他参数,那么必须使用括号围住,否则会抛出SyntaxError异常:

