1. 数据模型 data model
obj[key] -> obj.__getitem__(key)
__getitem__
可以读作 dunder-getitem (double-duner getitem),
这些 magic method 也称作 dunder method
列表:__len__
和 __getitem__
自定义的类只要实现了 __len__
和 __getitem__
这两个 magic method,就可以像 Python 数组一样使用,可以使用迭代、切片等 Python 提供的特性。
除了在定义 class 中常用到 __init__
以外,其他 magic method 都不是让你直接用的,要通过内置函数 len, iter, str等使用。
数值:__abs__
, __add__
and __mul__
from math import hypot # hypot 指 hypotenuse 斜边,用来算欧式距离
字符串:__str__
和 __repr__
__str__
和 __repr__
的区别
在 class 定义中,未定义 __str__
的时候会自动 fallback 到 __repr__
所以只写 __repr__
就够了。
__repr__
是为了消歧义,比如详细的告诉你这个类都有哪些参数,可以用这些信息还原原始的类,有些甚至可以直接用 eval 还原回来:
s="""w'o"w"""
eval(repr(s)) == s
>>> true
__str__
是给用户展示的,阅读起来友好
算数操作符:__add__
和 __mul__
分别代表加法和乘法
布尔值:__bool__
- 首先检查
__bool__
- 若不定义
__bool__
会检查__len__
,length 是 0 就是 False,其他为真。 - 若不定义
__bool__
和__len__
,自定义的类bool(my_class)
总是真。
更多特殊方法,见 原书第13页 Overview of Special Methods
take away
- 通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样 Pythonic
- Python 对象的一个基本要求就是它得有合理的字符串表示形式,用
__repr__
和__str__
- 模拟序列数据时会大量用到特殊方法
- 运算符重载
Data Structures
2. 序列
内置序列类型 sequence types
有两种分类方法:
- 保存的是值还是引用
- 可变还是不可变
Keeping in mind these common traits—mutable versus immutable; container versus flat—is helpful to extrapolate what you know about one sequence type to others.
超类在左边, 箭头从子类指向超类, 斜体名称代表抽象类和抽象方法
列表推导式 Listcomps 和生成器表达式 Generator Expression
- 列表推导式生成列表
- 生成器生成各种类型的序列
元组 tuple
元组是没有名字的记录(想象一个没有名字的 namedtuple),不仅仅是不可变的列表。
Star
*
运算符:
# 运算符可以把可迭代对象拆开作为函数的参数
def hello_star(a, b):
print(a, b)
t = (20, 8)
hello_star(*t)
>>> 20 8
# 还可以将收集到的参数放到列表里
a, b, *rest = range(5)
print(a, b, rest)
>>> 0 1 [2, 3, 4]
# 可以在各个位置
a, *rest, b = range(5)
print(a, b, rest)
>>> 0 4 [1, 2, 3]
具名元组 namedtuple
from collections import namedtuple
当作不可变列表的元组
元组不可变,没有列表增减元素的方法;元组也没有 __reversed__
方法,但实际上 reversed(my_tuple)
是合法的,😄 …
切片 slice
切片不包含最后一个元素,比如 mylist[:3] 是 0, 1, 2,不包括3,这样做是为了:
- stop - start 就是 length
- 切割序列很方便,mylist[:x] 和 mylist[x:] 就可以,没有重叠没有遗漏
修改切片?可以:
- 给切片赋值,就可以换掉所选的分片段儿:
l[2:5] = [20, 30]
- 删掉一段切片:
del l[2:5]
+
和 *
+
和 *
不修改操作对象,而是生成一个船新的序列。
注意:
<含有可变对象的序列> * 3
这种写法非常危险。
[详见第8章]
l = [['_']] * 3
l
>>> [['_'], ['_'], ['_']]
l[2][0] = 'X'
l
>>>[['X'], ['X'], ['X']]
# 只修改了最后一个 '_' 结果所有的都被修改了。
# 正确的写法
l = [['_'] for i in range(3)]
2.6 +=
和 *=
+=
-> __iadd__
(先尝试就地加法)-> __add__
(否则 fallback 到这里)
这里的区别在于,
- 使用
__iadd__
的情况:变量名a
指向的对象就地改动 - 使用
__add__
:先计算 a + b 得到新对象(a’),再将 a’ 赋值给 a,也就是说变量名a
会引用到新的对象
*=
和 +=
类似: *=
-> __imul__
-> __mul__
l = [1, 2, 3]
id_before_imul = id(l)
l *= 2
assert id(l) == id_before_imul
t = (1, 2, 3)
id_before_imul = id(t)
t *= 2
assert id(t) != id_before_imul
性能问题
对不可变序列(比如元组)进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
2.7 list.sort() 方法,内置函数 sorted()
- list.sort() 就地修改,返回值是 None
- sorted() 不修改操作对象,返回排好序的结果。sorted 甚至可以对不可变对象(元组等)排序
2.8 bisect
bisect.bisect
就是bisect.bisect_right
,是的,还有一个bisect_left
- left 和 right 的区别在于
bisect.bisect(HAYSTACK, 1)
得到的索引不同,按照 left 得到的索引插入 HAYSTACK,新元素会在与之相等的元素的左侧(left,也就是前面);right 在右侧(后面)。 - 排序很慢,如何在插入新数据时保持有序?使用
bisect.insort(HAYSTACK, 233)
2.9 有些情况有比列表更好的选择
- 只处理数字序列,使用
array
数组 - 频繁使用
append
和pop
? 你需要的是栈或队列,使用collections.deque
(双向队列)
take away:
- Python 中,成对的括号(
[]
,{}
,()
)之间的换行会被忽略 import array
真数组;注意这里严格区分了数组 array 和列表 listarray.array('h', [1, 2, 3])
中 h 表示短整数(16位)数组- 生成器可以省掉列表推导式中 for 循环的开销
- Raymond Hettinger 的 Python 代码片段
3. dict and set
3.1 Generic Mapping Types 泛用映射类型
collections.abc 中的 MutableMapping 和它的超类的 UML 类图(箭头从子类指向超类,抽 象类和抽象方法的名称以斜体显示)
collections.abc 中的 Mapping 和 MutableMapping 是抽象基类(ABC)。
ABC 不用于:自定义的 mapping 类型一般不会直接继承 ABC (即 Mapping/MutableMapping),而是直接继承 dict 或 collections.User.Dict。
ABC 用来:
- documenting and formalizing the minimal interfaces for mappings
- 判断数据是否是 mapping 类型:
from collections import abc; isinstance({}, abc.Mapping)
3.2 字典推导 dictcomp
3.3 mapping 类型的内置方法
值得注意的是
d.update(m, [**kargs])
,update 方法接受的 m 既可以是 mapping,也可以是键值对儿的迭代器(返回(k, v)
对儿的迭代器)- 有序字典可以用来实现,先进先出:
OrderedDict.popitem()
,后进先出:OrderedDict.popitem(last=False)
- setdefault 可以处理找不到的键(尤其是想要,先找到键,再往里插入数据)(如果只是单纯查找键,看 3.4 节)
occurrences = index.get(word, [])
occurrences.append(location)
index[word] = occurrences
# or
index.setdefault(word, []).append(location)
3.4 灵活的处理 Key Lookup
如果只是查key的时候想给缺的 key 返回默认值:
l = collections.defaultdict(list)
# 把 list 构造方法作为 default_factory 来创建 defaultdict 注意:l['missing_k1']
时才会调用default_factory
,l.get('missing_k2')
无效。- 利用
__missing__
自己处理缺失的 key
3.5 其他 mapping 类型
👆介绍了 dict 和 defaultdict,还有很多:
- collections.OrderedDict # 先进先出:
OrderedDict.popitem()
,后进先出:OrderedDict.popitem(last=False)
- collections.ChainMap # Py3 Only
- collections.Counter # 计数器,
most_common()
很好用 - collections.UserDict # 实现自己的字典的时候,最好继承这个类,见👇这条
3.6 继承 UserDict 实现自己的 mapping(不要继承 dict)
- UserDict.data 存放数据
- UserDict 继承自 MutableMapping
3.7 不可变的 mapping
利用 MappingProxyType 生成一个可变dict的镜像,这个镜像只能读,不能写。要修改数据需要直接修改原始的可变dict:
from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
d_proxy[1]
>>> 'A'
d_proxy[2] = 'x' # 只读的镜像
>>> TypeError: 'mappingproxy' object does not support item assignment
d[2] = 'B' # 只能通过原词典来修改数据
d_proxy[2]
>>> 'B'
3.8 集合
- set 不可散列,frozenset 可以散列
- 如果已经是集合了,要充分利用集合操作符,交集
&
,并集|
,差集-
:
# 快,且易读
found = len(needles & haystacks)`
# 慢
found = 0
for n in needles:
if n in haystacks:
found += 1
- 直接用字面量
{1, 2, 3}
比set([1, 2, 3])
快 - 集合推导 setcomp
3.9 dict 和 set 背后的技术
- dict 和 set 检索 key 的效率有多高?
- 为什么是无序的
- 为什么不可散列的对象不能当作 dict 的键和 set 的元素? # {[]} 会报 TypeError: unhashable type: ’list'
- 为什么不能在循环迭代 dict/set 的同时往里面加入新值?
4. Text and Bytes
decode 得到人类可读的文本, encode 成机器读的数字序列
4.1 basic && 4.2 bytes and bytearray
Py2 和 Py3 的区别:
- Py2 str 是原始字节序列 (raw bytes)
- Py2 的 unicode 和 Py3 的 str 都是 Unicode 字符的序列
- Py2 的 bytes 其实就是(Py2的)str 的别名 # assert isinstance(bytes(‘abc’), str)
- Py2 和 Py3 的 bytearray,以及 Py3 的 bytes 每个元素都是 0~255(含)的整数
- Py3
bytes.fromhex('31 4B')
,bytearray 也支持 - Py3
bytes(array.array('h', [1, 2, 3]))
# py2 和 py3
bytearray('abc')[0]
>>> 97
# py3
bytes('abc')[0]
>>> 97
# py2 的 bytes 其实就是 str
bytes('abc')[0]
>>> 'a'
# bytearray 没有字面量
# py2
bytearray('abc')[0:1]
>>> bytearray(b'a')
# py3
bytearray('abc', encoding='utf8')[0:1]
>>> bytearray(b'a')
4.3 编码器,解码器
for codec in ['gb2312', 'gbk', 'utf8', 'utf-16le']:
try:
print(codec, '氣'.encode(codec), sep='\t')
except UnicodeEncodeError:
print('%s not support' % codec)
>>>
gb2312 not support
gbk b'\x9a\xe2'
utf8 b'\xe6\xb0\xa3'
utf-16le b'#l'
gb2312
不支持繁体字,但是大多数网站会把 gb2312 链接到 gbk,参考这里
4.4 编码问题
- UnicodeEncodeError 从 str 到 bytes 的时候出错:
city.encode('cp437', errors='replace')
把无法编码的字符替换成 ‘?’ - UnicodeDecodeError 从 bytes 到 str 出错:
octets.decode('utf_8', errors='replace')
无法解码的字符用 � (REPLACEMENT CHARACTER) 代替,表示未知字符 - SyntaxError 输入的编码格式和预期不符: 比如py3引入了一个非 utf8 编码的module, py2 引入了utf8编码的module
Q:那如何知道用了什么编码方式吗?
A:理论上不可能,但是因为字节流可能有规律可以猜测出来(用 chardet
)
Q:BOM 是什么?
A:Byte-order mark. 字节流开头的 b'\xff\xfe'
就是 BOM,用来表示编码使用 Intel CPU 小字节序。
Q:为什么要区分大端小端?
A:有一些码位需要多个字符来表示,比如 é 对应到 \xe9\x00
,由于历史原因,有一些机器要把 \x00
放在前面,有一些放在后面,如何进行区分呢(字节序)?这时就要用到 BOM 字节序标记。
Q:什么时候不需要区分大小端? A:
- 一个字只占一个字符的时候(比如 ASCII)
- 生成的字节序列始终一致的编码,比如 UTF8
(注:尽管 UTF8 不需要 BOM,但是 Windows 的记事本仍然会在 UTF8 文件中添加 BOM(utf8编码下的 U+FEFF 是 b'\xef\xbb\xbf'
,所以看到一个文件如此开头,很可能是带 BOM 的 UTF8 编码文件。
Q:大端小端的区别是什么?
A:小字节序机器:最低有效字节在前面,也就是 \xe9\x00
;大字节序机器顺序正好相反,是 \x00\xe9
Q:如何区分大端小端? A:
- 直接指定用大端(
utf_16be
)还是小端(utf_16le
) - 没有指定的时候,在字节流的头部加上 BOM,
\xff\xfe
(255,254) 表示小端,\xfe\xff
(254, 255)表达大端。 - 没有指定,也没有 BOM,按照标准应该假定为大端(用UTF-16BE编码),但是由于 Intel 使用小端,事实上很多没有BOM的文件用的是 UTF-16LE 编码
(补充,\xff\xfe
或者 \xfe\xff
叫做 ZERO WIDTH NOBREAK SPACE(U+FEFF),在UCS(通用编码集)中没有定义,专门用来区分大小端)
4.5 处理文本的最佳实践
- 如果代码要在多种操作系统下运行,不要依赖默认编码!open 文件时应明确地指定编码
- 打开文件的默认编码见
locale.getpreferredencodng()
- sys.stdout/stdin/stderr 的默认编码见
PYTHONIOENCODING
环境变量 - 文件名(而不是文件内容)见
sys.getfilesystemencoding()
4.6 要比较 unicode 字符串,要先进行规范化
- 比如 1/2 和 ½,同一个字母的各种变种等
- str.casefold() 大致和 str.lower() 结果一致
- 去掉变音符号,比如 é 上的尖
4.7 Unicode 排序
- 用
locale.strxfrm
对非 ASCII 文本排序 - 而在使用
strxfrm
之前要设置区域(locale.setlocale
),但是区域设置是全局的,所以不要在库中调用 setlocale - 可以设置时区的格式要看操作系统,比如 Unix 是 language_code.encoding (语言码.编码),中文是 zh_CN.UTF-8
import locale
locale.setlocale(locale.LC_COLLATE, 'zh_CN.UTF-8')
>>> 'zh_CN.UTF-8'
fruits = ['苹果', '香蕉', '菠萝', '葡萄', '橘子', '柿子']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
sorted_fruits
>>> ['柿子', '橘子', '苹果', '菠萝', '葡萄', '香蕉']
或者使用 PyUCA 库
import pyuca
coll = pyuca.Collator()
sorted_fruits = sorted(fruits, key=coll.sort_key)
4.8 Unicode 数据库
包含各种 unicode 字符的元数据信息
4.9 同时支持字符串和字节序列的 API
主要有 re
和 os
:
- re 中
rb'\d+'
只能识别 ASCII 数字,而r'\d+'
支持 Unicode 数字
The GNU/Linux kernel is not Unicode savvy, so in the real world you may find filenames made of byte sequences that are not valid in any sensible encoding scheme, and cannot be decoded to str. GNU/Linux 内核不理解 Unicode, 因此你可能发现了,对任何合理的编码方案来说,在文件名中使用字节序列都是无效的,无法解码成字符串。
翻译有误
传入字节序列就返回字节序列文件名,字符串就是字符串文件名:
# str 文件名
os.listdir('.')
>>> ['Fluent Python.txt', '流畅的 Python.txt']
# 在不知道文件名是什么编码的时候,可以直接读取 bytes 文件名,再猜测编码
os.listdir(b'.')
>>> [b'Fluent Python.txt', b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84 Python.txt']
# fsdecode
for i in os.listdir(b'.'):
print(i, os.fsdecode(i), sep=', ')
>>> b'Fluent Python.txt', Fluent Python.txt
>>> b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84 Python.txt', 流畅的 Python.txt
# fsencode
for i in os.listdir('.'):
print(i, os.fsencode(i), sep=', ')
>>> Fluent Python.txt, b'Fluent Python.txt'
>>> 流畅的 Python.txt, b'\xe6\xb5\x81\xe7\x95\x85\xe7\x9a\x84 Python.txt'
Further Reading
- Dive into Python3 #15
- Python Cookbook, 3e
三、作为对象的函数
5. 函数是一等对象
整数、字符串、字典和函数都是一等对象。
一等对象的条件:
- 在运行时创建
- 能赋值给变量或者数据结构中的元素
- 能作为参数传给函数
- 可以作为函数的返回结果
5.1 验证函数是一等对象
- 在 console session,在运行时创建函数
- 函数对象有属性,比如
__doc__
- 函数对象是 function 类的实例
- 函数对象可以做其他函数的参数:
list(map(factorial, range(10)))
5.2 高阶函数
定义:接受函数做参数,或者返回值是函数的函数是高阶函数。
比如:
- map
- filter
- reduce, 主要用来求和,现在最好直接用
sum
py2 py3:
- py2 中的 filter/map 返回 list,可以替换为用列表推导式
- py3 中的 filter/map 返回生成器,可以替换为用生成器表达式
-
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。(see also #10.6)
5.3 lambda 匿名函数
限制:
- 只能用纯表达式(pure expression),不可以赋值,不可以用 while,try之类的语句
- lambda 基本上只做参数传给高阶函数
5.4 可调用对象 Callable Object 有七种
符号
调用运算符 ()
可调用对象:
- 用户定义的函数:
def xxx
- 内置函数:C 语言实现的函数,
len
- 内置方法:C 语言实现的方法,
dict.get
- 方法:类中定义的函数
- 类:比如说在用
myclass=MyClass()
生成实例的时候,就直接调用了MyClass
- 类的实例:可以在类中定义
__call__
方法从而让实例可以直接调用 - 生成器函数:
yield
Q:如何判断是不是可调用对象? A:用内置的 callable() 函数:
callable(MyClass)
>>> True
5.5 自定义的可调用类型
实现 __call__
方法即可
5.6 函数内省
自省是这种能力:检查某些事物以确认它是什么、它知道什么以及它能做什么
具体到 Python 函数就是说,定义一个函数(line *
)之后:
def f(a, b): # *
pass
print(f.func_code.co_varnames) # **
('a', 'b')
可以通过函数对象拿到定义函数时,函数名,函数的参数(line **
)等等信息。[参考1]
内省除了可以得到这些定义函数时的信息之外,由于函数是一个对象(Function 类的实例),实际上你可以给它定义用户属性(line *
):
f.whatever = 'hello' # *
f.__dict__ # **
>>> {'whatever': 'hi'}
而用户自定义的属性可以像类一样,通过 __dict__
得到(line **
)。
5.7 位置参数和关键字参数
Py3 还支持只能用关键字参数传参的参数,只需要放到带 * 的参数
后面即可(*args
会收集所有的位置参数,当然如果根本不会有位置参数,可以只用一个 *
):
def f(a, *, b):
return a, b
# b 只能用关键字参数的方式传参(强制传入实参 b)
f(1, b=2)
5.8 获取函数的参数信息
比如 bobo (一个 web 框架)利用函数内省:
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
启动
bobo -f bobo_demo.py -p 9090
可以这样调用:
http localhost:9090/ person=amy
http localhost:9090/?person=amy
需要重点关注的是,bobo 是怎么知道 hello 函数需要 person
参数,并且去 request form 里去寻找这个参数?
通过自省。
具体而言,要获取函数参数信息,需要在多个地方寻觅:
__code__
中是参数__defaults__
中有位置参数和关键字参数的默认值__kwdefaults__
中有 keyword-only 参数的默认值
非常麻烦,所以直接用 inspect
模块比较好:
from inspect import signature
sig = signature(myfunc)
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
inspect.Signature
对象有一个 bind 方法,可以传入一些参数,然后 bind 会和 Signature 实例中的参数进行匹配,实际上就是检查参数是否合格。
5.9 函数注释(函数说明,函数注解)
Py3 新功能
Python 对注解所做的唯一的事情是, 把它们存储在函数的 annotations 属性里。 仅此而已, Python 不做检查、不做强制、不做验证,什么操作都不做。
主要目的是让 IDE 进行静态类型检查
5.10 函数式编程
operator 库中的函数:
- mul:
lambda a, b : a* b
- itemgetter:
lambda field: fields[1]
- attrgetter:
lambda someobj: someobj.whatever
- methodcaller: 类似
functools.partial
用 reduice 实现求和的功能,可以直接用 sum
。
但是求累积只能自己实现:
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
👆 的 lambda 只是求了一个积而已,可以直接用 operator 库提供的算数运算符函数 mul(也就是说这个函数实际上实现了 *
乘法操作符):
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
还有常用 lambda 实现的需求是从序列中取出特定位置的值:lambda fields: fields[1]
不过用 operator.itemgetter
会更好:
from operator import itemgetter
for city in sorted(metro_data, key=itemgetter(1)):
...
类似的还有一个用来取属性的函数 operator.attrgetter
。
operator 中还有一个常用的函数是 methodcaller
from operator import methodcaller
some_string = 'Time flies!'
# 去掉感叹号
remove_bang = methodcaller('replace', '!', '')
remove_bang(some_string)
>>> 'Time flies'
参考资料:
6. 用一等函数实现的设计模式
skip for now (2018-06-21)
7. 装饰器和闭包
要理解装饰器的原理要先理解闭包,而闭包要用到 Py3 引入的一个新关键字
nonlocal
基础问题:
- 看懂装饰器语法
- 如何判断是否是局部变量
- 为什么要有闭包,闭包的工作原理
进阶问题:
- 实现装饰器
- 看看标准库中的一些装饰器
- 实现有参数的装饰器
7.1 装饰器 101
- 装饰器是语法糖
- 特性一:把被装饰的函数替换成其他函数
- 特性二:装饰器在加载模块的时候执行
def deco(func):
def inner():
print('inside inner()')
return inner
@deco
def target():
print('outside target()')
# 语法糖
target()
# 相当于:
def target():
print('outside target()')
deco(target)() # 这里直接将 target 这个函数对象作为参数传入
7.2 装饰器何时执行?
run right after the decorated function is defined.
被装饰的函数被定义之后(line *
)马上执行:
@deco
def target(): # *
...
7.3 用装饰器改进 6.1 中的例子
其实就是把所有的 promo 在定义的时候(通过加上 @promotion)自动加到促销列表里…
7.4 变量作用域
在函数体中使用未声明的变量:
def f1(a):
print(a)
print(b)
f1(233)
>>> NameError: global name 'b' is not defined
在函数体中使用全局变量:
b = 6
f1(233)
>>> 3
>>> 6
在函数体中,使用「全局变量」(1
)然后在函数体内声明同名的局部变量(2
):
在编译时,因为函数体内声明了 b,Python 认为它是局部变量。
b = 6
def f2(a):
print(a)
print(b) # <1>
b = 9 # <2>
f2(233)
>>> 233
>>> UnboundLocalError: local variablle 'b' referenced before assignment
在函数体中,显式声明使用全局变量(1
):
b = 6
def f3(a):
global b
print(a)
print(b)
b = 9
f3(233)
>>> 233
>>> 9
7.5 闭包 Closures
a closure is a function with an extended scope that encompasses nonglobal variables referenced in the body of the function but not defined there 闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的 非全局变量。 a closure is a function that retains the bindings of the free variables that exist when the function is defined, so that they can be used later when the function is invoked and the defining scope is no longer available 闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时, 虽然定义作用域不可用了,但是仍能使用那些绑定。
需求: 实现一个可以记录调用历史的函数,比如记住传入的所有参数,每次返回平均值。
实现1:
用类。将调用历史存在属性中,再定义一下 __call__
成为可调用对象。
class Averager:
def __init__(self):
self.nums = []
def __call__(self, num):
self.nums.append(num)
return sum(self.nums)/len(self.nums)
avg = Averager()
avg(10)
>>> 10.0
avg(11)
>>> 10.5
实现2:
用闭包。将调用历史存在自由变量 (2
)里。
def make_averager():
nums = [] # <1>
def avg(num):
nums.append(num) # <2>
return sum(nums)/len(nums)
return avg
avg = make_averager() # <3>
avg(10)
>>> 10.0
avg(11)
>>> 10.5
看一下自由变量:
局部变量名在 co_varnames
(1
), 自由变量的名字 nums
在 co_freevars
(2
)。 而自由变量的值则保存在 __closure__
中 (3
)
avg.__code__.co_varnames
>>> ('num',) # <1>
avg.__code__.co_freevars
>>> ('nums',) # <2>
avg.__closure__ # <3>
>>> (<cell at 0x10f5f9b28: list object at 0x10f7a9548>,)
avg.__closure__[0].cell_contents
>>> [10, 11]
7.6 nonlocal
声明
👆 的实现得到的 avg 每次调用都要重新做一次求和再用除法计算均值,可以优化一下,每次只保存上次的平均值和调用次数即可。
重新实现一下,这次只保存上次的均值和调用次数:
def make_averager():
count = 0
total = 0
def averager(num):
count += 1
total += num
return total/count
return averager
注意:当 count 是数字或任何不可变类型时, count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成 局部变量。
要解决这个问题有两个方法:
- 显式声明 total 和 count 是自由变量,可以使用 Py3 的
nonlocal
(1
) - 将不可变类型的数据放到可变类型里用作自由变量 (
2
), Py2 适用
# 1
def make_averager():
count = 0
total = 0
def averager(num):
nonlocal total, count # <1>
count += 1
total += num
return total/count
return averager
# 2
def make_averager():
data = {'count': 0, 'total': 0} # <2>
def averager(num):
data['count'] += 1
data['total'] += num
return data['total']/data['count']
return averager
7.7 简单装饰器
import time
def clock(func):
def clocked(*args): # <1>
t0 = time.perf_counter()
result = func(*args) # <2>
elapsed = time.perf_counter() - t0
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, func.__name__, arg_str, result))
return result
return clocked # <3>
注意:
2
用的 func 是 clock 的参数,也即 clock 函数体内的局部变量(对于 clocked 来说是自由变量)@clock
相当于把装饰的函数,传给 clock 作为参数4
:
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
# ===
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial) # <4>
7.8 标准库中的装饰器
装饰类方法的装饰器:
- property #19.2
- classmethod # 9.4
- staticmethod # 9.4
functools.wraps 是用来写装饰器的小工具
可以复制原始函数的 __name__
和 __doc__
到装饰器返回的函数。还可以正确处理关键字参数。
7.8.1 functools.lru_cache
用来缓存会反复计算的值,已节约计算资源
@functools.lru_cache() # <1>
@clock
def fibonicci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
注意到 1
用到的 lru_cache()
用了括号,是因为 lru_cache
可以接受配置参数:
functools.lru_cache(maxsize=128, typed=False)
其中 maxsize 是最多存储多少个值(应该是 2 的幂),typed=True 表示要分别保存浮点数和整数(比如 1.0 和 1)。
<问题> 那不需要配置参数的就不用括号,是因为括号被省略了吗?
(Python Cookbook)9.6 定义一个能接收 可选参数的装饰器”一节中的装饰器可以作为常规的装饰器调用,也可以作为装饰器工厂 函数调用,例如 @clock 或 @clock()
7.8.2 单分派的泛用型函数
(Py2 中要使用需要安装 pip install singledispatch
)
7.9 叠放使用多个装饰器(注意顺序)
@d1
@d2
def f():
print('f')
# ===
def f():
print('f')
f = d1(d2(f))
7.10 接受参数的装饰器
装饰器函数只接受一个参数(就是要修饰的函数),所以要实现接受参数的装饰器需要在外面再包一层,也就是用装饰器工厂函数。
从概念上看,这个新 的 register 函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是 应用到目标函数上的装饰器。
第一个例子,改进 #7.2 中的 register
registry = set()
def register(active=True): # 装饰器工厂函数
def decorate(func): # 装饰器
print('running register(active=%s) -> decorate(%s)' % (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
第二个例子,改进 clock 装饰器
…
四、面向对象
8. 对象引用,可变性,垃圾回收
variables are lables, not boxes. 变量是标签,不是盒子。
Q:为什么元组不可变,但是元组中的值可变?
勘误
If reference variables are old news to you, the analogy may still be handy if you need to explain aliasing issues to others. 如果你不知道引用式变量是什么,可以像这样对别人解释别名。 如果你已经了解引用式变量,还可以用这个比喻向别人解释别名。
8.1 变量不是盒子,而是便利贴
变量 a 和 b 引用同一个列表,而不是那个列表的副本
8.2 标识(label,便利贴,别名),相等性和同一性(是否是一个东西)
equality and identity
p187 译文有误: > 复制对象时,相等性和标识之间的区别有更深入的影响
8.3 默认是浅拷贝(可能存在问题)
l1 = [2, [233, 234, 235], ('x', 'y', 'z')]
l2 = list(l1)
l1.append('L1')
l2 # <1>
l1[1].append(236)
l2 # <2>
l1[2] += ('a', 'b', 'c')
l2 # <3>
- 给 l1 添加元素,对 l2 没有影响;
- 如果l1元素是可变的,因为浅拷贝只复制了元素的引用,导致对 l1 的修改同时会影响到 l2;
- 如果l1元素是不可变的,比如 tuple,+= 操作相当于创建了一个新 tuple(对象),再重新绑定回元素,不会影响到 l2。
[2, [233, 234, 235], ('x', 'y', 'z')] # <1>
[2, [233, 234, 235, 236], ('x', 'y', 'z')] # <2>
[2, [233, 234, 235, 236], ('x', 'y', 'z')] # <3>
用 copy 模块的 deepcopy
来实现深复制
如何实现浅拷贝?
- 对于内置的可变序列类型:直接使用内置的类型构造方法,比如
l2 = list(l1)
或l2 = l1[:]
- 其他对象使用
copy.copy
: MyObj2 = copy.copy(MyObj)
如何实现深拷贝?
copy.deepcopy
8.4 函数参数使用引用的问题
Python 的参数传递模式是共享传参(call by sharing),各个形式参数获得实参中各个引用的 copy
Call by sharing means that each formal parameter of the function gets a copy of each reference in the arguments. the parameters inside the function become aliases of the actual arguments.
函数内的形参是实际参数的别名。
或者说,对象 A ---> 贴标签到对象A(即实参a) ---> 传入函数内,再给对象 A 贴标签(即形参 a')
⚠️ 函数参数如果是可变类型的,要格外小心
- 参数默认值不要用可变对象!
- 如果定义的函数就是接收可变参数,那么在函数内对可变参数的修改是否要带到函数外部,需要函数的定义方要和函数的调用方达成共识。默认不要修改通过参数传入的对象!
8.5 del 和垃圾回收
- del 删除名称,del 不删除对象。
- del 如果删除了对象所有的引用,该对象可能会被 GC
8.6 弱引用
学习使用 weakref
weakref.ref()
是低层接口,不要直接使用。WeakValueDictionary
可变映射,存储的值是对象的弱引用。被引用的对象被gc之后,对应的key会自动从 WeakValueDicitonary 删除。适合做缓存。WeakKeyDictionary
可变映射,键是弱引用。WeakSet
# 以下在 Python 控制台,iPython 不行
import weakref
a_set = {0, 1}
wref = weakref.ref(a_set) # 创建 a_set 的弱引用
wref
>>> <weakref at 0x10a553578; to 'set' at 0x10a4293f0>
wref()
>>> {0, 1}
a_set = {2, 3, 4} # 修改 a_set 指向的对象,应该会导致 wref 失效
wref()
wref() is None # 但是控制台中 `_` 变量会指向上一个非 None 的表达式结果(也就是 {0, 1}),导致 wref 指向的对象仍然存在
>>> False
a_set # 刷掉 `_` 中的 {0, 1}
wref() is None
>>> True
>>> {0, 1}
CPython 中弱引用的所指对象(弱引用的目标)的限制:
- 🚫 list 和 dict 不可以
- list/dict 的子类可以:
class MyList(list):
- set 本身就可以
- 用户自定义类型可以
- int 和 tuple 甚至其子类都不可以
take away 🌮
- 纯函数式编程中,所有数据都是不可变的。
9. Pythonic 对象
自定义类型的行为可以像内置类型一样自然,依赖的是「鸭子类型」
9.1 对象表示形式
将对象表示成字符串:
- repr() 会调用
__repr__
- str() 会调用
__str__
用其他姿势表示对象:
__bytes__
__format__
9.2 以第一章的向量为例
目标,让 Vector 类实现以下功能 (v1 = Vector2d(3, 4)
):
- 分量可以直接通过属性访问: assert v1.x == 3
- 拆包成元组:x, y = v1
- repr 得到的「源码」可以直接构造出 Vector:assert eval(repr(v1)) == v1 (这里只是举例,最好直接用 copy.copy)
- 支持直接用
==
比较 - print 函数会调用 str,格式对人类友好
- bytes 函数会调用
__bytes__
,生成二进制形式 - abs 函数会调用
__abs__
,计算向量的模 - bool 函数会掉用
__bool__
,如果模为0返回 False,其他为 True
class Vector2d:
typecode = 'd' # 类属性
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
9.3 新增一种构造方法
灵感来源,标准库的 array.array 有个类方法 .frombytes
...
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)
9.4 classmethod 和 staticmethod
- classmethod 操作的是类本身,而普通method操作的是类的实例。
- classmethod 的第一个参数一定是 cls(类本身)
- staticmethod 只是碰巧在类定义中出现的函数定义,没有什么场景是一定要用 staticmethod 的(本来也不是特别有用)
class Demo:
@classmethod
def classmtd(*args):
print(args)
@staticmethod
def staticmtd(*args):
print(args)
d = Demo()
d.classmtd('hello')
>>> (<class '__main__.Demo'>, 'hello')
d.staticmtd('hello')
>>> ('hello',)
9.5 格式化显示 Formatted Displays
format(some_obj, format_spec)
str.format()
背后都是 __format__
魔法方法。
9.6 让对象可散列 (hashable)
hashable 有几个要求:
- 实现
__hash__
方法 - 实现
__eq__
方法 - 不可变
9.7 私有属性只是君子协议
通过名称改写(name mangling)把双下划线开头的属性的名字改了
比如:Dog.__food -> Dog._Dog.__fod
9.8 用 __slots__
节省内存
__slots__
可以使用任何可迭代对象,最好用不可变的 tuple。
slots 注意事项⚠️:
- 就算父类有
__slots__
,子类也要定义一下,因为__slots__
不会继承 - 实例中只有
__slots__
中列出的属性,当然,可以把__dict__
放到__slots__
里(这样就不省内存了) __weakref__
加到__slots__
里才能弱引用这个实例
9.9 类属性
类属性可以作为实例属性的默认值。
class Dog:
name = 'whatever'
def intro(self):
print("hi, I'm %s" % self.name)
10. 序列,hashing 和切片
- 协议
- 接口
- 鸭子类型
10.1 自定义一个序列类型
Vector 类
10.2 支持任意维数的 Vector 第一版(兼容 Vector2d)
class Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components)
def __iter__(self):
return iter(self._components)
def __repr__(self):
components = reprlib.repr(self._components)
components = components[components.find('['):-1]
return 'Vector({})'.format(components)
...
- 用形如 [1, 2, 3] 的列表来初始化 Vector
- 维度信息保存在 array (数组)中
- repr 表示的时候,去掉无用的 ‘array’ 字样,这些是内部细节不需要暴露出去
10.3 协议,鸭子类型 Protocols and Duck Typing
协议是非正式的接口,只在文档定义,不在代码中定义。
10.4 支持切片的 Vector 第二版
要支持序列,把序列操作直接交给 Vector 类中的序列来做就行了(比如,self._components 数组,天然支持序列操作)
比如通过把操作委托(delegate)给 Vector 类中的数组,来实现 __len__
和 __getitem__
。
class Vector:
...
def __len__(self):
return len(self._components)
def __getitem__(self, index):
return self._components[index]
有一个缺陷,看出来了吗?
留意这两个方法的返回值类型! 返回的并不是 Vector 类,而是数组(array)!
怎么解决?
- 用返回的 Array 再构造 Vector 类(不好,现在的 Vector 构造方法是接受一个 list,或者通过 frombytes 用二进制创建,并不支持通过 Array 创建)
- 直接在 Vector 类上实现
__len__
and__getitem__
切片的实现原理
⚠️ 学习探索的思路
第一步,看一下 __getitem__
是如何工作的。可以发现它生成了 slice 对象。
class MySeq:
def __getitem__(self, index):
return index
s = MySeq()
s[1]
>>> 1
s[1:4]
>>> slice(1, 4, None)
s[1:4:2]
>>> slice(1, 4, 2)
s[1:4:2, 9]
>>> (slice(1, 4, 2), 9)
s[1:4:2, 7:9]
>>> (slice(1, 4, 2), slice(7, 9, None))
看一下 slice 对象: dir(slice)
,其中有一个 indices 方法:
slice(None, 10, 2).indices(5)
>>> (0, 5, 2)
是说,给定一个 len 是 5 的序列,给定 [:10,2] 的切片,会修正为 [0:5:2],
比如:'ABCDE'[:10:2]
等于 'ABCDE'[0:5:2]
。
indices 方法的用途就是处理缺失的索引值(比如 start 没有给出),处理超长的切片(比如 end 到 10 了,而总长只有5)。
class Vector:
def __len__(self):
return len(self._compontents)
def __getitem__(self, index):
cls = type(self)
if isinstance(index, slice):
return cls(self._components[index])
elif isinstance(index, numbers.Integral):
return self._components[index]
else:
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls))
注意,上面的错误信息是从 'ABC'[1,2]
的的错误 TypeError: string indices must be integers, not tuple
抄来的。
为了创建 Pythonic 的对象,要模仿内置的对象的行为。
10.5 支持动态存取属性的 Vector 第三版
回顾一下:
class Vector2d:
typecode = 'd' # 类属性
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
可以方便的通过 v.x 获取分量,现在的 Vector 构造方法是传入一个列表,如何用 x,y,z 来代替 v[0], v[1], v[2] 呢?
最直接的办法,是写 x,y,z,t 四个属性,但是太麻烦了,代码都是重复的。可以使用 __getattr__
来增加对这四个属性的支持:
class Vector:
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self)
if len(name) == 1:
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, name))
说明一下,
- 只允许单字母的 xyzt 这四个名字
- 其他情况抛出 AttributeError
上面的实现有一个 bug,
v = Vector(range(5))
v
>>> Vector([0.0, 1.0, 2.0, 3.0, 4.0])
v.x
>>> 0.0
v.x = 10
v.x
>>> 10
v
>>> Vector([0.0, 1.0, 2.0, 3.0, 4.0])
- 不应该允许对单个分量进行赋值,这里缺少限制
- 而当对分量赋值了之后,只修改了分量属性值,改动没有传递到 Vector(v 的值没有变)。
回忆一下属性查找顺序,当 v.x = 10
的时候,给实例增加了 x 属性,下次再取 x 属性时,直接获取了实例属性,根本不会走到 __getattr__
这一步。
how to fix?
实现一下 __setattr__
对 v.x = 10
这种增加新实例属性的行为进行限制:
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1:
if name in cls.shortcut_names: # 不允许给 xyzt 这些属性赋值
error = 'readonly attribute {attr_name!r}'
elif name.islower(): # 不允许给小写字母属性名赋值
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = '' # 不在限制之列的新属性可以添加
if error:
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) # 新属性通过父类的方法创建
不过还有一种方法可以限制新建实例属性,还记得吗?
就是 __slots__
,但是只有在需要节省内存的时候再使用它,切记。这里不推荐使用。
10.6 可散列的 Vector 第四版
Vector V3 实现了通过 xyzt 获取前四个分量的值,但是不允许通过 xyzt 进行赋值。要允许修改分量,要使用
__setitem__
方法实现 v[0] = 1.1 形式的赋值,或者使用__setattr__
实现 v.x = 1.1 形式的赋值。
这里不允许修改分量,因为如果要让 Vector 是 hashable 的,Vector 必须是不可变的。
这里实现一下 __hash__
:
class Vector:
def __hash__(self):
hashes = (hash(x) for x in self._components) # <1>
return functools.reduce(operator.xor, hashes, 0) # <2>
有两点注意:
- 这里的生成器表达式可以换成 map:
hashes = map(hash, self._components)
(map 在 Py2 中效率较低,因为要构建一个列表) - reduce 用到了第三个参数(initializer)。如果 iterable 是空的,就会直接返回 initializer(代码更健壮),iterable 不为空的话,initializer 就是第一个参数(所以对于
+|^
应该用 0,对于*&
应该用 10)。
继续利用 reduce 思想,把 __eq__
修改一下:
之前的实现对于高维的 Vector 效率很低,因为需要完全复制两个 Vector 进行比较:
def __eq__(self, other):
return tuple(self) == tuple(other)
实际上完全可以逐个比较两个 Vector 的各个分量,只要发现有分量不同,就可以提前知道两个 Vector 相异:
def __eq__(self, other):
if len(self) != len(other): # <1>
return False
for a, b in zip(self, other): # <2>
if a != b:
return False
return True
注意两点:
- 在用 zip 逐个比较分量之前,一定要先比较长度 ⚠️,因为 zip 在一个输入先耗尽之时不会抛出异常
- 这里可以简化成:
all(a == b for a, b in zip(self, other))
10.7 格式化输出的 Vector 第五版
除了笛卡尔坐标,这里还支持了球面坐标
take away
repr()
用来输出有用的调试信息,不应该抛出异常。- 属性查找顺序:
my_obj.x -> 查找实例的 x 属性 -> 查找类的 x 属性 -> 查找父类的 x 属性 -> 通过 __getattr__(self, 'x') 尝试获取 x 属性
- 不建议为了避免新增实例属性而使用
__slots__
。 最好只用它来节省内存。 - 一般来说,如果实现了
__getattr__
方法,最好也定义一下__setattr__
,防止对象行为不一致 itertools.zip_longest
有一个可选参数 fillvalue 可以填补缺失的值,也就是说,如果 zip 在一起的两个序列,较短的那个耗尽之后,就会输出 fillvalue(默认是 None)。- 本章的延伸阅读非常值得思考 💭 go to devonthink, go to source
11. 接口
抽象类表示接口。An abstract class represents an interface. 本章只是学习抽象基类的工作原理。 不建议你自己编写抽象基类,因为很容易过度设计。
11.1 Python 中接口和协议的定义
接口:对于抽象基类以外的类,类自己实现或继承的公开属性(方法或数据属性),包括特殊方法就是接口。
接口补充定义:对象公开方法的子集,让对象在系统中扮演特定的角色。(接口是实现特定角色的方法集合)
协议:协议是非正式的接口。 协议补充说明:协议不能像正式接口那样施加限制。一个类可能只实现了部分接口。
对于 Python 来说,「X 类对象」「X协议」「X接口」是一个意思。
11.2 序列协议
序列协议是最基础的协议之一。就算对象只实现了部分协议,Python 解释器也会尝试进行处理(补足缺失的实现)。
from collections import Iterable
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]
f = Foo()
isinstance(f, Iterable) # 并不是可迭代对象
>>> False
20 in f # 虽然没有定义 __contains__ 方法,Python 解释器仍然尝试用 __getitem__ 让 __contains__ 可用
>>> True
for i in f: print(i) # 没有定义 __iter__,Python 解释器进行了处理
>>> 0
>>> 10
>>> 20
len(f)
>>> TypeError: object of type 'Foo' has no len() # 没有实现 __len__ 方法
鉴于序列协议的重要性, 如果没有
__iter__
和__contains__
方法, Python 会调用__getitem__
方法,设法让迭代和 in 运算符可用。
11.3 猴子补丁 Monkey-Patching
以第一章的扑克牌类为例:
from random import shuffle
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
deck = FrenchDeck()
shuffle(deck)
>>> TypeError: 'FrenchDeck' object does not support item assignment # 不支持元素赋值
def set_card(deck, position, card): # <1>
deck._cards[position] = card
FrenchDeck.__setitem__ = set_card # 动态添加了元素赋值方法
shuffle(deck)
deck[:5]
[Card(rank='4', suit='spades'),
Card(rank='2', suit='spades'),
Card(rank='8', suit='diamonds'),
Card(rank='A', suit='spades'),
Card(rank='4', suit='clubs')]
猴子补丁:在运行时修改类或模块(不需要修改源码)。
但是补丁要非常要处理的代码,比如这里的 set_card (1
) 函数要知道 deck 的数据存在 _cards 属性中。
11.4 一个水禽的例子
引入一个概念「鹅类型」
白鹅类型指, 只要 cls 是抽象基类, 即 cls 的元类是 abc.ABCMeta, 就可以使用 isinstance(obj, cls) 。
但是,哪怕是抽象基类,也不能滥用 isinstance 检查,用得多了表明面向对象设计得不好。
应不应该使用 isinstance 检查抽象基类?
- 不应该的情况:在一连串 if/elif/elif 中使用 isinstance 做检查,然后根据对象的类型执行不同的操作,通常是不好的做法;此时应该使用多态,即采用一定的方式定义类,让解释器把调用分派给正确的方法。
- 应该的情况:要求强制执行 API 约定时
11.5 定义抽象基类的子类
- 直接继承抽象基类即可
class FrenchDeck2(collections.MutableSequence):
- 要实现抽象基类中的抽象方法(即抽象基类并没有实现的方法,需要继承者自己实现)
- 抽象基类中已实现的具体方法可以直接继承来直接用(废话,就跟普通继承一样)
- 子类根据自己的情况可以替换抽象基类中的方法,使用更高效的自己的实现
11.6 看看标准库里有什么抽象基类
标准库里有两个 abc 模块:
- collections.abc (我们主要用这个)
- abc.ABC (用来创建新的抽象基类,见 11.7 的例子)
其他抽象基类:
- numbers:比如
isinstance(x, numbers.Integral)
11.7 自己创建一个抽象基类(只是了解工作原理,不要自己这么干!)
take away
- Python 会特殊对待看起来像是序列的对象。 Python 中的迭代是鸭子类型的一种极端形式:为了迭代对象,解释器会尝试调用两个不同 的方法。
12. 继承
Java 不支持剁成继承
12.1 不要直接子类化内置类型
C 语言编写的内置类型(dict,list..) 不会调用用户定义的子类覆盖的特殊方法。
要子类化 dict,list 和 str,应该继承 collections 中的 UserDict, UserList 和 UserString
12.2 多重继承的顺序
class A:
def ping(self):
print('ping A:', self)
class B(A):
def pong(self):
print('pong B:', self)
class C(A):
def pong(self):
print('PONG C:', self)
class D(B, C): # B 在 C 前面:现在 B 里面找,找不到再去看 C
def ping(self):
super().ping()
print('post-ping D:', self)
def pingpong(self):
self.ping()
super().ping() # 内置的 super() 会按照 __mro__ 属性给出的顺序调用超类的方法
self.pong()
super().pong()
C.pong(self)
12.3 多重继承的例子一:Tkinter
12.4 多重继承最佳实践
- 把接口继承和实现继承区分开
- 使用抽象基类显式表示接口
- 通过混入重用代码
- 在名称中明确指明混入
- 抽象基类可以作为混入,反过来则不成立
- 不要子类化多个具体类
- 为用户提供聚合类
- 「优先使用对象组合,而不是类继承」
12.5 多重继承的例子二:Django
与 Tkinter 相比, Django 基于类的视图 API 是多重继承更好的示例。尤其是,入类易于理解:各个混入的目的明确,而且名称的后缀都是 …Mixin 。 需要新功能时,很多 Django 程序员依然选择编写单块视图函数,负责处理所有事务,而不尝试重用基视图和混入。
take away
- 若想把方法调用委托给超类,推荐的方式是使用内置的 super(),不过有时可能需要绕过方法解析顺序,直接调用某个超类的方法——这样做有时更方便。
- 分析类时,我经常在交互式控制台中查看
__mro__
属性:def print_mro(cls): print(', '.join(c.__name__ for c in cls.__mro__))
13. 运算符重载
运算符重载的作用是让用户定义的对象使用中缀运算符(如
+
和|
)或一元运算符(如-
和~
)。
所以可以解决一个问题:
Vector2d.__eq__
存在一个 bug, assert Vector(3, 4) == [3, 4]
,这不合理。
13.1 Python 中的运算符重载的限制
- 不能重载内置类型的运算符
- 不能新建运算符,只能重载已有的
- 逻辑运算符 is and not or 不可以重载
13.2 一元运算符
- 取负
-
__neg__
- 取正
+
__pos__
- 取反
~
__invert__
- 取绝对值
abs()
__abs__
补充文档
要支持一元运算符只需要实现背后的特殊方法,而且特殊方法只有一个 self 参数,不过最好不要就地修改,而是返回一个新的。
13.3 重载加法运算符 +
目的,让两个 Vector 相加的结果是,各个分量相加,如果 Vector 的长度不同,较短的那个用 0 填充。
from vector_v5 import Vector # https://github.com/fluentpython/example-code/blob/master/10-seq-hacking/vector_v5.py#L201
# monkey patch __add__
def add_v1(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a+b for a, b in pairs)
Vector.__add__ = add_v1
v1 = Vector([3, 4, 5])
v1 + (10, 20, 30)
>>> Vector([13.0, 24.0, 35.0])
(10, 20, 30) + v1
>>> TypeError: can only concatenate tuple (not "Vector") to tuple
# monkey patch __radd__
def radd(self, other):
return self + other
Vector.__radd__ = radd
# monkey patch __add__ again,为更符合 Python 习惯,异常时返回 NotImplemented(单例值)
def add_v2(self, other):
try:
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
except TypeError:
return NotImplemented
Vector.__add__ = add_v2
运算符分派机制
简单说,会先尝试调用左操作数上的 __add__
方法,结果是 NotImplemented 单例值时,再尝试调用右操作数上的 __radd__
。
13.4 重载乘法运算符 *
import numbers
from vector_v5 import Vector
def mul(self, scalar):
if isinstance(scalar, numbers.Real): # 这就是白鹅类型的实际运用,显式检查抽象类型
return Vector(n * scalar for n in self)
else:
return NotImplemented
def rmul(self, scalar):
return self * scalar
Vector.__mul__ = mul
Vector.__rmul__ = rmul
v1 = Vector([1.0, 2.0, 3.0])
15 * v1
>>> Vector([15.0, 30.0, 45.0])
13.5 比较运算符
比相等,和比大小。
和 __add__
__radd__
有一些区别:
__eq__
的反向操作符仍然是__eq__
(参数对调了);正向的__gt__
换成反向的__lt__
,参数也要对调。==
和!=
如果连反向调用都失败了,不会抛 TypeError,而是比较对象 id
1. __eq__
之前实现的 Vector 有一个问题是 assert Vector([1, 2]) == [1, 2]
,不合理。
修改一下 __eq__
加一个限制:
def __eq__(self, other):
if isinstance(other, Vector):
return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))
else:
return NotImplemented
Vector([1, 2]) == (1, 2)
>>> False
2. __ne__
从 object 中继承的 __ne__
就够用了,一般不需要自己实现。object.__ne__
会直接对 __eq__
的结果取反。
13.6 增量赋值的 +=
和 *=
- 没有实现就地操作符(比如
__iadd__
),a+=b
只是a = a + b
的语法糖。这时候不需要其他代码,只要有__add__
就够了。 - 如果要实现就地操作,就要实现
__iadd__
。 - 不可变类型一定不能实现就地操作特殊方法。(比如 tuple)
take away
- 实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。
- (实现
__add__
时)如果由于类型不兼容而导致运算符特殊方法无法返回有效的结果, 那么应该返回 NotImplemented , 而不是抛出 TypeError。返回 NotImplemented 时,Python 会尝试调用反向方法(__radd__
)。 - (同上)为了支持其他类型, 我们返回特殊的 NotImplemented 值(不是异常),让解释器尝试对调操作数,然后调用运算符的反向特殊方法(如
__radd__
)。 - 如果操作数的类型不同,我们要检测出不能处理的操作数。本章使用两种方式处理这个问题:一种是鸭子类型,try/catch TypeError 异常;另一种是显式使用 isinstance 测试(比如
__mul__
) - 矩阵乘法中缀运算符
@
是 Python3.5 引入的。背后的特殊方法是__matmul__
__rmatmul__
__imatmul__
。 +=
比+
对第二个操作数更宽容:my_list + x
要求 x 也是 list,my_list += x
和list.extend(x)
只需要 x 是可迭代对象。- 重要提醒:增量赋值特殊方法(
__iadd__
)必须返回 self。 functools.total_ordering
是个类装饰器(Python 2.7 及以上版本可用), 它能为只 定义了几个比较运算符的类自动生成全部比较运算符。
五、控制流程
14. 可迭代对象,迭代器,生成器
生成器都是迭代器(因为生成器实现了迭代器接口)。不过《设计模式》中区分了迭代器(用于从集合中取元素)和生成器(用于凭空产生新元素)。但是 Python 不区分迭代器和生成器。
14.1 单词序列,Sentence 第一版
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)
Sentence 第一版,是序列,也可以迭代,原因在于 iter()
iter 的作用:
- 检查是否有
__iter__
方法,有则调用来创建迭代器 - 否则,检查是否有
__getitem__
方法,有则创建迭代器,尝试从 0 开始按顺序取值 - 否则 raise TypeError
14.2 可迭代对象 vs 迭代器
可迭代对象的定义:
只要实现特殊的 __iter__
方法, 或者实现 __getitem__
方法且 __getitem__
方法的参数是从 0 开始的整数(int), 都可以认为对象是可迭代的。
可迭代对象包括:
使用 iter() 可以获取迭代器的对象,就是可迭代对象; 一个对象实现了可以返回迭代器的
__iter__
就是可迭代对象; 序列都是可迭代对象; 实现了__getitem__
方法且参数从 0 开始索引,也是可迭代对象。
Python 从可迭代对象中获取迭代器。
举个例子,字符串 ‘ABC’ 是可迭代对象,每次遍历的时候都会从这个可迭代对象生成的迭代器取值。
s = 'ABC'
for char in s:
print(s)
# 相当于
it = iter(s) # 生成迭代器
while True:
try:
print(next(it))
except StopIteration: # <1>
del it # 删除迭代器
break
Python 会处理好 for 循环和其他迭代(列表推导,元组拆包等)中的 StopIteration 异常 (1
)。
迭代器接口 interface for iterator
有两个方法:
__next__
,返回下一个可用元素,元素耗尽就 raise StopIteration__iter__
,返回 self。这样在应该使用可迭代对象的地方,就可以使用迭代器了 (比如for)。
迭代器接口定义:
可以看到,iterator(迭代器)继承自 Iterable 抽象基类。__iter__
是在 Iterator 中定义的,就是直接返回 self:
def __iter__(self):
return self
迭代器定义:
实现了无参数的 __next__
方法 和 __iter__
方法的对象(因为实现了 __iter__
所以迭代器都是可迭代对象)。
迭代器和可迭代对象的联系:
迭代器都是可迭代对象。 iter(<可迭代对象>)
可以得到迭代器。
可迭代对象要求有 __iter__
方法,每次都实例化一个新的迭代器;
迭代器要要求实现__next__
和 __iter__
(返回迭代器本身)方法。
迭代器是可迭代对象,但是可迭代对象不是迭代器。
![生成器家族.png](resources/01F01ED854921712B90631D4EB418E55.png =574x203) 来自这里和这里
14.3 《设计模式》中的迭代器,Sentence 第二版
这里实现的是《设计模式》中描述的迭代器:
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
可以明显看到 SentenceIterator 中大量代码都是在处理状态,比如 index 数字。
是不是在想为什么要把新写一个 SentenceIterator 啊,把 __next__
直接在 Sentence 中实现不行吗?
这是个糟糕的想法!结果会导致 Sentence 的实例既是可迭代对象也是自身的迭代器…
《设计模式》中讲到,迭代器可以用来「支持对聚合对象的多种遍历」,多种遍历是指可以从一个可迭代对象中多次获取独立的迭代器,比如可迭代对象 l = [1, 2, 3]
可以多次生成状态相互独立的迭代器 iter(l)
。
可迭代对象一定不能是自身的迭代器,可迭代对象必须实现 __iter__
但是不能实现 __next__
。
14.4 生成器函数,Sentence 第三版
用 SentenceIterator 比较麻烦,更 Pythonic 的方法是,用生成器函数替代它:
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
return # 可省略
上面的 __iter__
还可简化成:
def __iter__(self):
return iter(self.words)
这个 __iter__
方法是生成器函数,调用时会返回生成器对象(实现了迭代器接口)。
生成器函数里面一般会有循环,不过不是必要的,这里就连续用了多个 yield …
14.5 惰性求值的 Sentence 第四版
把第三版的 re.findall
换成惰性求值的 re.finditer
即可。
class Sentence:
def __init__(self, text):
self.text = text
def __iter__(self):
for match in RE_WORD.finditer(self.text):
yield match.group()
...
14.6 生成器表达式,Sentence 第五版
生成器表达式是语法糖:完全可以替换成生成器函数。
class Sentence:
def __init__(self, text):
self.text = text
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
14.7 生成器函数 vs 生成器表达式
处于可读性考虑,如果生成器表达式需要多行代码,最好写成生成器函数。
14.8 一个生成等差数列的生成器
take 1 直接写一个生成器类:
class ArithmeticProgression:
def __init__(self, begin, step, end=None):
self.begin = begin
self.step = step
self.end = end
def __iter__(self):
result = type(self.begin + self.step)(self.begin)
forever = self.end is None
index = 0
while forever or result < self.end:
yield result
index += 1
result = self.begin + self.step * index
ap = ArithmeticProgression(0, 1, 3)
list(ap)
take 2 写一个生成器函数
def aritprog_gen(begin, step, end=None):
result = type(begin + step)(begin)
forever = end is None
index = 0
while forever or result < end:
yield result
index += 1
result = begin + step * index
ap = aritprog_gen(0, 1, 3)
list(ap)
take 3 利用 itertools 中的工具
import itertools
def aritprog_gen(begin, step, end=None):
first = type(begin + step)(begin)
ap_gen = itertools.count(first, step)
if end is not None:
ap_gen = itertools.takewhile(lambda n: n < end, ap_gen)
return ap_gen
14.9 标准库中的生成器函数
os.walk, itertools 和 functools 等
- 用于过滤,产出输入的可迭代对象的子集
- 映射,map
- 合并,chain
- 拓展,产出比输入还多的元素
- 重新排列,groupby,反转(reverse)
14.10 yield from
Python 3.3 之后的版本才有。
yield from 有两个功能:
- 语法糖,代替一层 for 循环
- 见第16章
def chain(*iterables):
for it in iterabels:
for i in it:
yield i
# 可以用 yield from 取代内层的 for
def chain(*iterables):
for it in iterables:
yield from it
14.11 可迭代的归约函数 Iterable Reducing Functions
表中所有内置的归约函数都可以用 functools.reduce 实现,内置是因为比较常用。
而且,all 和 any 会短路。
14.12 iter() 的罕见用法
给 iter 传入两个参数,第一个是可调用对象(比如函数),第二个是哨符(中止值)。当可调用函数返回值等于哨符时,raise StopIteration。
from random import randint
def d6(): # 函数不接受任何参数
return randint(1, 6)
d6_iter = iter(d6, 1) # d6_iter 是 callable_iterator 可调用迭代器!
for roll in d6_iter:
print(roll)
14.13 真实案例
14.14 生成器用作协程
见第 16 章
take away
- 序列可以迭代的原因是实现了
__getitem__
方法(如果是自己写最好也实现一下__iter__
) - how to 检查是否可迭代:iter(x) 不可迭代的话会有 TypeError。如果用
isinstance(x, abc.Iterable)
会漏掉实现了__getitem__
的对象 - how to 检查是否是迭代器:
isinstance(x, abc.Iterable)
(p.336) - abc.Iterable 实现了
__subclasshook__
也就是说只要有__iter__
isinstance(x, abc.Iterable) 就是 True,无需注册
15. 上下文管理器, else
其他语言罕见的控制流程特性,with 和(其实应该是 then 的) else
15.1 其实用 then 更合适的 else
for/else
for 没有被 break 中止,正常结束之后运行 else
while/else
while 没有被 break 中止,因为条件不再为真结束之后运行 else
try/else
try 没有抛出异常时才会运行 else
所有上面三种情况下,如果有异常或者 return,break,continue 导致控制权跳到了复合语句的主块之外,else 都会被跳过。
15.2 上下文管理器, with
上下文管理器协议:
__enter__
,with 语句开始运行时调用之__exit__
,相当于 finally
with open('xxx.py') as fp:
中 with 后的表达式 (open()
)的结果是上下文管理器对象,而在上下文管理器对象上调用 __enter__
的结果是 fp
。
with 语句中的 as 语句是可选的(对于 open 来说 as 是必选的,这样才能得到文件句柄)。
class LookingGlass:
def __enter__(self):
import sys
self.original_write = sys.stdout.write
sys.stdout.write = self.reverse_write
return 'AAAAABBBBB'
def reverse_write(self, text):
self.original_write(text[::-1])
def __exit__(self, exc_type, exc_value, traceback): # <1>
import sys
sys.stdout.write = self.original_write
if exc_type is ZeroDivisionError:
print('DO NOT divide by zero!')
return True # <2>
1
如果没有异常,这里的三个异常参数都是 None;如果有异常,可以通过 try/finally 中的 finally 块中调用 sys.exc_info 得到
2
如果 __exit__
返回 True,表明没有异常/异常已处理,这时解释器会压制异常;返回其他值表明异常没有处理(如果有异常的话),而且会向上冒泡
如果
__exit__
方法返回 None ,或者 True 之外的值, with 块中的任何异常都会向上冒泡。
15.3 标准库中的 contextlib
closing
supress
@contextmanager
ContextDecorator
用于定义基于类的上下文管理器时用的基类ExitStack
这个上下文管理器能进入多个上下文管理器。
15.4 @contextmanager
不需要写一个类来实现 __enter__
和 __exit__
,只需要写一个生成器,yield 之前的部分在 __enter__
时执行,yield 之后的部分在 __exit__
时执行。
import contextlib
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
yield 'AAAAAABBBBBBB' # <1>
sys.stdout.write = original_write
with looking_glass() as what:
print("1, 2, 3")
print(what)
>>> 3 ,2 ,1
>>> BBBBBBBAAAAAA
what
>>> AAAAAABBBBBBB
👆的例子有一个严重的 bug,如果在 with 块中出现异常,解释器会先捕获,然后在 1
这一行重新 raise,造成 sys.stdout.write = original_write
这一步无法执行,导致 sys.stdout.write 无法恢复到原始状态:
with looking_glass() as what:
print(1/0)
print("1, 2, 3") # 已经在 with 之外了,但是 sys.stdout.write 仍然没有恢复
>>> 3 ,2 ,1
所以要修复也很简单,把 yield 这一行 try/catch 住:
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
msg = ''
try:
yield 'AAAAAABBBBBBB' # <1>
except ZeroDivisionError:
msg = 'DO NOT divide by zero!'
finally:
sys.stdout.write = original_write
if msg:
print(msg)
with looking_glass() as what:
print(1/0)
>>> DO NOT divide by zero!
yield 这一行(1
)会将值绑定到 as 子句的目标上(也就是 what
)。
15.2 中实现的上下文管理器类中的 __exit__
方法在处理完异常之后会返回 True,表示异常已处理,解释器就会压制异常;
这里使用 @contextmanager 时默认的行为却是相反的:解释器默认认为所有异常都已经被处理了,会压制异常,如果要异常传递出去,就在被装饰的函数中重新抛出异常。
take away
- EAFP 取得原谅比获取许可容易 (多 try/except)
- LBYL 三思而后行(多 if 判断)
- with 语句的目的就是简化 try/finally,便于做事后清理。
- with 块没有定义新的作用域
- 在 @contextmanager 装饰器装饰的生成器中, yield 与迭代没有任何关系。在本节所举的示例中,生成器函数的作用更像是协程:执行到某一点时暂停,让客户代码运行,直到客户让协程继续做事。
- with 不仅能管理资源,还能用于去掉常规的设置和清理代码, 或者在另一个过程前后执行的操作 What Makes Python Awesome?
16. 协程
从根本上把 yield 视作控制流程的方式,这样就好理解协程了。
16.1 从生成器到协程
PEP342 之后 yield 可以在表达式中使用,而且新增了 .send() 方法。
coroutine: a procedure that collaborates with the caller, yielding and receiving values from the caller. 协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
PEP380 生成器支持 return value 了(之前 return 不能有操作数,否则 SyntaxError);新增了 yield from
16.2 协程 101
简单协程,只 yield 一次
def sc():
print('-> coroutine started')
x = yield
print('-> coroutine received:', x)
my_sc = sc()
next(my_sc) # <1>
>>> -> coroutine started
my_sc.send(233) # <2>
>>> -> coroutine received: 233
>>> StopIteration:
- 调用函数得到生成器对象之后,要先调用 next() (
1
)让生成器推进到 yield 这里; - 因为 yield 没有操作数(相当于 yield None),所以 yield 本身没有输出到 stdout;
- 调用 send 之后,协程继续运行,
yield None
的值是 42,绑定到 x 上(即 x = yield),协程会一直运行到下一个 yield 处或者终止(raise StopIteration)。
yield 两次的协程
def simple_coro2(a):
print('-> Started: a =', a)
b = yield a # <1>
print('-> Received: b =', b)
c = yield a + b
print('-> Received: c = ', c)
In [45]: my_coro2 = simple_coro2(233)
In [46]: next(my_coro2)
-> Started: a = 233
Out[46]: 233
In [47]: my_coro2.send(42) # <2>
-> Received: b = 42
Out[47]: 275
# 以下省略
赋值语句中,等号(=)右侧的代码在赋值之前先执行,所以对于 b = yield a (1
行),先执行 yield a(结果是把 a 打印到 stdout),再将 yield a 的结果(也就是 send 过来的参数,对于 2
就是 42)赋值给 b
simple_coro2 的调用过程分为三步:
- next(my_coro2): 第一个 print ,yield a
- my_coro2.send(28):send 的参数 28 赋值给 b,第二个 print,yield a + b,
- my_coro2.send(99):send 的参数 99 赋值给 c,第三个 print,触底 StopIteratoration
16.3 协程 201,继续之前计算均值的例子
第 7 章用闭包实现过一个计算均值的例子,这里用协程再来一次:
def averager():
total = 0.0
count = 0
average = None
while True: # <1>
term = yield average
total += term
count += 1
average = total/count
coro_avg = averager()
next(coro_avg) # prime
coro_avg.send(10) # 之后不断的 send 值进去求均值
>>> 10.0
coro_avg.send(30)
>>> 20.0
问题:1
这里的 while True 无限循环,何时才能终止呢?
16.4 不想每次都手动 next() 怎么办?预先激发协程
可以实现一个装饰器来自动 prime:
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
来应用到 16.3 的 averager 上:
@coroutine # 只多了这一行
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
存在的陷阱:
- 本装饰器和 yield from 不兼容(因为 yield from 会自动 prime) 参考 16.7
16.5 如何终止协程,以及异常处理
如何终止协程? 1. send 一个会导致协程内部出现异常的值
coro_avg = averager()
coro_avg.send(10)
>>> 10
coro_avg.send('hi')
>>> TypeError: unsupported operand type(s) for +=: 'float' and 'str'
inspect.getgeneratorstate(coro_avg) # 可以看到这时协程以及停止了
>>> 'GEN_CLOSED'
2. send 一个预先约定好的值,协程收到这种值就退出(一般用 None,Ellipsis,还有人用 StopIteration 类本身)
@coroutine
def averager_2():
total = 0.0
count = 0
average = None
while True:
term = yield average
if term is Ellipsis:
break
total += term
count += 1
average = total/count
coro_avg2 = averager_2()
coro_avg2.send(10)
>>> 10.0
coro_avg2.send(30)
>>> 20.0
coro_avg2.send(Ellipsis)
>>> StopIteration:
3. 用 throw
或 close
方法显式地把异常传给协程
throw 可以传入任何异常,如果协程不能 handle 这种异常,异常就会向上传递,到达调用方这里
close 会在协程内暂停的 yield 表达式这里抛出 GeneratorExit:
- 如果生成器没有处理 GeneratorExit 或者抛出了 StopIteration(比如执行到协程末尾了),不会有异常传递出去
- 收到 GeneratorExit 之后,生成器不能 yield 值出去,否则会造成 RuntimeError
问题: 如果无论如何都想在协程结束之后进行一些清理工作怎么办? 再加上一层 try/finally 即可:
def averager_2():
total = 0.0
count = 0
average = None
try:
while True:
term = yield average
if term is Ellipsis:
break
total += term
count += 1
average = total/count
finally:
print('-> coroutine ending')
16.6 让协程能够返回值
下面的例子特别的地方在于,没有 yield 值出去,只是在最后 return 结果。而最后的结果中不光有平均数,还有统计的数字的个数。
from collections import namedtuple
Result = namedtuple('Result', 'count average')
@coroutine
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Result(count, average)
coro_avg = averager()
coro_avg.send(10) # 没有值 yield 出来
coro_avg.send(30)
coro_avg.send(50)
coro_avg.send(None)
>>> StopIteration: Result(count=3, average=30.0) # 返回值作为异常对象的 value 返回了...
可以看到协程 return 值的方式比较奇葩,值是作为异常对象的 value 返回的… 稍微改进一下,显式取出异常的 value:
coro_avg = averager()
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(50)
try:
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
result
>>> Result(count=3, average=30.0)
16.7 yield from
when a generator gen calls yield from subgen(), the subgen takes over and will yield values to the caller of gen; the caller will in effect drive subgen directly. Meanwhile gen will be blocked, waiting until subgen terminates. yield from (或者 Python3.5 新引入的 await):在生成器 gen 中使用
yield from subgen()
时, subgen 会获得控制权,并 yield 值 (verb)给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。
名词解释:
- delegating generator 委派生成器:包含
yield from <iterable>
的生成器函数 - subgenerator 子生成器:从
yield from <iterable>
中的 iterable 获取的生成器 - caller 调用方。这里有歧义:
- 调用委派生成器的调用方(客户端)<—
- 调用子生成器的调用方(就是委派生成器本人)
16.8 yield from 的原理(意义)
16.9 用协程做离线事件仿真
出租车队仿真的例子,感觉很像 Effective Python 中生命游戏?
take away
- 查看协程状态:
inspect.getgeneratorstate()
- 我见过的 yield from 示例几乎都使用 asyncio 模块做异步编程,因此要有有效的事件循环(active event loop)才能运行。
- 维基百科几乎是学习任何计算机科学知识的入门首选。
17. 用 futures 并发 / Concurrency with Futures
future 对象是 concurrent.futures 和 asyncio 的基础。
17.1 三种姿势来下载
对于 I/O 密集型应用,并发下载的吞吐量更高
第一种,顺序下载
pass
第二种,使用 concurrent.futures
MAX_WORKERS = 20
def download_one(cc):
image = get_flag(cc)
save_flag(image, cc.lower() + '.gif')
return cc
def download_many(cc_list):
workers = min(MAX_WORKERS, len(cc_list)) # 避免浪费线程
with futures.ThreadPoolExecutor(workers) as executor: # <1>
res = executor.map(download_one, sorted(cc_list))
return len(list(res))
executor.__exit__
(1
) 会调用 executor.shutdown(wait=True)
,它在所有线程执行完之前阻塞线程 (which will block until all threads are done)。
「阻塞」是指 挡住不继续执行(等待线程都执行完)再放行?
Futures 是什么?在哪里?
Future 是什么?
Future 表示终将发生的事情,而确定某件事会发生的唯一方式是执行的时间已经排定。 a Future represents something that will eventually happen, and the only way to be sure that something will happen is to schedule its execution.
换句话说,要给要做的事情排定执行的时间,具体的作法就是把这件事情交给 concurrent.futures.Executor
进行处理,Executor
会创建 concurrent.futures.Future
实例。
举个例子:
Executor.submit()
接受一个可调用对象参数,为对象排期后会返回 Future。
Future 的状态只能由并发框架修改,客户端代码不应干预。在 Future 代表的要做的事情完成之后,并发框架才会修改 Future 状态,至于要做的事情何时会做完,客户端无法控制。
Future (类)在哪里?
标准库中的 concurrent.futures.Future
和 asyncio.Future
。
Future 的内置方法
.done()
其实应该叫is_done
,返回 Bool 告诉你 Future 完事了没;.add_done_callback()
只有一个参数,接受可调用对象,在 Future 完事之后调用该对象;.result()
- Future 完事之后调用,concurrent.futures.Future 和 asyncio.Future 作用相同:返回要做的事情完成的结果(如有异常就抛出);
- Future 完事之前调用:
- concurrent.futures.Future 会阻塞调用方所在线程直到 Future 完事返回结果(有个可选的
timeout
参数,过时不候,抛出 TimeoutError) - asyncio.Future 不会阻塞,直接抛出 asyncio.InvalidStateError(所以也不存在 timeout 参数)
- concurrent.futures.Future 会阻塞调用方所在线程直到 Future 完事返回结果(有个可选的
as_completed
函数,参数是 Future 列表,返回迭代器,在 Future 完事之后 yield Future(takes an iterable of futures and returns an iterator that yields futures as they are done.)
Future 实例长什么样子?
def download_many(cc_list):
cc_list = cc_list[:5]
with futures.ThreadPoolExecutor(max_workers=3) as executor:
to_do = []
for cc in sorted(cc_list):
future = executor.submit(download_one, cc)
to_do.append(future)
print('Scheduled for {}: {}'.format(cc, future))
results = []
for future in futures.as_completed(to_do):
res = future.result()
print('{} result: {!r}'.format(future, res))
results.append(res)
return len(results)
可以看到每次 futures.as_completed() 产出的 future 的顺序(是否可以说 future 完成顺序?)是都不是固定的。
目前实现 download_many 的并发脚本都不能并行下载:
- concurrent.futures 版本受 GIL 限制
- asyncio 版本在单个线程中运行
17.2 阻塞性 I/O 和 GIL / Blocking I/O
CPython 解释器本身不是线程安全的,因此有全局解释器锁(GIL),一次只允许使用一个线程执行 Python 字节码。
什么是 Blocking I/O?
17.3 concurrent.futures 的多进程
ProcessPoolExecutor 实现了和 ThreadPoolExecutor 通用的 Executor 接口,因此可以很方便的把多线程转换成多进程。
唯一值得注意的区别是:
ProcessPoolExecutor 的 max_workers
参数是可选的,而且大多数情况下不会用,默认就是 os.cpu_count()
返回的 CPU 数。
17.4 进一步了解 Executor.map
def say_hi(n):
print('%s' % '\t' * n)
return n
>>> from concurrent.futures import ThreadPoolExecutor
>>> executor = ThreadPoolExecutor(3)
>>> results = executor.map(say_hi, range(5)) # <1>
0
1
2
3
4
>>> results
<generator object Executor.map.<locals>.result_iterator at 0x10d662830>
>>> for i, result in enumerate(results): # <2>
print(i, result)
0 0
1 1
2 2
3 3
4 4
executor.map 的结果是一个生成器(1
);
从这个生成器(results)取值时(2
),for 会隐式调用 next(results),而 next 会在表示第一个要做的事的 Future (比如叫 _f
) 上调用 _f.result()
,result 方法是会阻塞,直到要做的事完事了。
Executor.map 的一个特点:
返回结果的顺序和调用开始的顺序一致。
缺点: 这样有一个后果,没办法尽早获取执行更快的任务的结果,比如十个任务,第一个任务最耗时(阻塞了 10s),那在取 results 的时候就要先等 10s 等待第一个任务完成,而其他任务不会阻塞(因为早在 10s 之前就完成了)。
另一个缺点(限制):
没有 submit 和 as_completed
组合来得灵活:
- submit 可以接受不同的可调用对象和参数(map 只能处理同一个可调用对象,每次传入不同的参数)
as_completed
接受各种类型的 Executor 实例,比如一些来自 ThreadPoolExecutor 一些来自 ProcessPoolExecutor
take away
- Cpython 解释器为什么不是线程安全的
- GIL 对 I/O 密集型基本无害;而 CPU 密集型可以用多进程绕开 GIL
18. 用 asyncio 并发
Concurrency is about dealing with lots of things at once. Rarallelism is about doing lots of things at once. Not the same, but related.
asynicio 使用事件循环驱动的协程实现并发。
18.1
asyncio.Task vs. threading.Thread:
先看 threading 版本: pass
再看 asyncio 版本:
import asyncio
import itertools
import sys
@asyncio.coroutine
def spin(msg):
write, flush = sys.stdout.write, sys.stdout.flush
for char in itertools.cycle('|/-\\'):
status = char + ' ' + msg
write(status)
flush()
write('\x08' * len(status))
try:
yield from asyncio.sleep(.1)
except asyncio.CancelledError: # 这个 Exception 由 cancel 引发(告诉 spin 是时候结束了)
break
write(' ' * len(status) + '\x08' * len(status))
@asyncio.coroutine
def slow_function():
yield from asyncio.sleep(3)
return 42
@asyncio.coroutine
def supervisor():
spinner = asyncio.async(spin('thinking!')) # 给 spin 排期,返回 Task 包装的 spin 协程
print('spinner object:', spinner)
result = yield from slow_function()
spinner.cancel() # 手动终止 spin 协程(会引发 CancelledError)
return result
def main():
loop = asyncio.get_event_loop() # 这就是事件循环!!!
result = loop.run_until_complete(supervisor())
loop.close()
print('Answer:', result)
asyncio.Future vs. concurrent.futures.Future
两者接口基本一致,但是实现方式不同,不可以互换。
补充一句,👆 讲的 asyncio.Task 是 asyncio.Future 的子类。
asyncio.Future 和 concurrent.futures.Future 最大的不同是 .result()
方法的行为不同:
- asyncio.Future 的 result 不接受参数(自然不能用 timeout),而且如果调用 result 时 Future 还没完事,直接 raise asyncio.InvalidStateError,根本不会阻塞等待 Future 搞定。
- asyncio.Future 一般不用 result 拿到 result(结果,双关,😄),而是用 yield from(类似 concurrent.futures.Future 的
add_done_callback
)
既然结合 yield from 使用,asyncio.Future 一般不用 my_future.add_done_callback
和 my_future.result
Task
得到 Task:
asyncio.async(coro_or_future)
BaseEventLoop.create_task(coro)
# Python3.4.2 or above
18.2 asyncio 版下载国旗
Python3.4 起,asyncio 只直接支持 TCP 和 UDP。 要实现 HTTP 客户端和服务器,大家都使用 aiohttp 包(pip install aiohttp)
基本的流程是一样的:在一个单线程程序中使用主循环依次激活队列里的协程。各个协程向前执行几步,然后把控制权让给主循环,主循环再激活队列里的下一个协程。
下面的例子在 Py3.6 下报错。
需要把 aiohttp.request()
改成 aiohttp.ClientSession().get(url)
。
import asyncio
import aiohttp
from flags import BASE_URL, save_flag, show, main
@asyncio.coroutine
def get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = yield from aiohttp.request('GET', url)
image = yield from resp.read()
return image
@asyncio.coroutine
def download_one(cc):
image = yield from get_flag(cc)
show(cc)
save_flag(image, cc.lower() + '.gif')
return cc
def download_many(cc_list):
loop = asyncio.get_event_loop()
to_do = [download_one(cc) for cc in sorted(cc_list)]
wait_coro = asyncio.wait(to_do) # 不阻塞。wait 是协程,等传给它的所有协程都完事了才结束
res, _ = loop.run_until_complete(wait_coro) # 阻塞。执行第一行创建的事件循环(loop),直到 wait_coro 结束
loop.close()
return len(res)
main(download_many)
如何理解 yield from?
眯着眼,假装 yield from 不存在。这样和非并发的依序下载代码一样易于阅读:
before
@asyncio.coroutine
def get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = yield from aiohttp.request('GET', url)
image = yield from resp.read()
return image
after
def get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = aiohttp.request('GET', url)
image = resp.read()
return image
yield from 到底做了什么?
参考 #16.7
yield from foo 不会阻塞,因为当前协程(就是 yield from foo)暂停后,控制权回到 event loop,event loop 继续驱动下一个协程;
foo Future/协程完事之后,把结果返回暂停的协程(yield from foo),继续执行。
yield from 怎么用?
- 使用 yield from 串联起来的多个协程最外面必须由客户端代码(不是协程)驱动,客户端显式或隐式(比如 for 循环)在最外层的委派生成器上调用
next()
或send()
方法 - yield from 链条最内层的子生成器必须是简单生成器(只用 yield)或者可迭代对象
- 用 asyncio 包时,最外层委派生成器传给 asyncio API 的函数(比如
loop.run_until_complete()
)进行驱动 - 用 asyncio 包时,最内层的协程通过 yield from 把任务交给 asyncio 包中的协程函数(或协程方法,比如
yield from asyncio.sleep()
)或者其他库中实现高层协议的协程(比如aiohttp.request('GET', url)
) 也就是说,最内层的子生成器是 asyncio(或 aiohttp) 中真正执行 I/O 操作的函数,不是我们自己写的!
比如:
def main():
loop = asyncio.get_event_loop() # event loop
result = loop.run_until_complete(supervisor()) # 最外层的委派生成器(supervisor)传给 asyncio API 函数来驱动
loop.close()
print('Answer:', result)
18.3 Running Circling Around Blocking Calls 避免阻塞型调用
什么是阻塞型函数?
Ryan Dahl(Node.js 作者)把 执行硬盘或网络 I/O 操作的函数定义为阻塞型函数
如何避免阻塞型调用阻塞主进程?
- 把所有阻塞型操作都丢到各自独立的线程中
- 把阻塞型操作转换成非阻塞的异步调用
方法1 的问题是,每个操作系统线程(OS thread,Python 用的就是这种线程)要占用上 MB 的内存。如果要处理上千个连接,内存都耗不起。
方法2 如何实现?使用回调实现异步调用。
回调类似于硬件中断。使用回调时,不等待响应,而是注册一个函数,当预订的事情发生时调用它。
只有异步应用程序底层的事件循环能依靠基础设施的中断、线程、轮询和后台进程等,确保多个并发请求能取得进展并最终完成,这样才能使用回调。
补充一个方法3,协程。对于事件循环来说,调用回调函数 <1> 和在暂停的协程上调用 .send()
方法 <2> 效果类似。(暂停的协程只占用很少的内层,协程还能避免回调地域)
为什么同样是单线程 flags_asyncio.py 比 flags.py 快 5 倍?
flags.py 依序下载,每次下载要等几十亿个 CPU 周期等待;
flags_asyncio.py
中的 download_many
调用 loop.run_until_complete
时,事件循环驱动各个 download_one
协程,执行到第一个 yield from (yield from get_flag(cc)
),再驱动各个 get_flag
协程,执行到第一个 yield from(yield from aiohttp.request('GET', url)
),调用 aiohttp.request()
。这些调用都不阻塞,所以在零点几秒内所有下载请求都开始了。asyncio 的基础设施拿到第一个响应之后,事件循环把响应发给 get_flag
协程,get_flag
向下执行到第二个 yield from(yield from resp.read()
),调用 resp.read()
,然后把控制权还给事件循环。因为下载请求几乎是同时发出的,其他响应会陆续返回。等到 get_flag
协程都获得结果之后,委托生成器 download_one
继续执行,保存图像文件。
18.4
take away
- 真正的并行需要多个CPU核心。
yield from asyncio.sleep(.1) 代替
time.sleep(.1)` 这样的 sleep 不会阻塞事件循环 (sleep without blocking the event loop)。- 在 asyncio 协程中使用 time.sleep 会阻塞主进程,从而冻结事件循环。
- 小工具,在命令行中测试 Future 和协程:
import asyncio
def run_sync(coro_or_future):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro_or_future)
- p454,Guido van Rossum 给出的技巧,轻松地理解示例 18-5 的总体逻辑:眯着眼,假装没有 yield from 关键字。这样做之后,你会发现示例 18-5 中的代码与纯粹依序下载的代码一样易于阅读。
- 每个操作系统线程(OS thread,Python 用的就是这种线程)要占用上 MB 的内存