jingyi's blog

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__

更多特殊方法,见 原书第13页 Overview of Special Methods

take away

Data Structures

2. 序列

内置序列类型 sequence types

有两种分类方法:

  1. 保存的是值还是引用
  2. 可变还是不可变

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,这样做是为了:

  1. stop - start 就是 length
  2. 切割序列很方便,mylist[:x] 和 mylist[x:] 就可以,没有重叠没有遗漏

修改切片?可以:

  1. 给切片赋值,就可以换掉所选的分片段儿:l[2:5] = [20, 30]
  2. 删掉一段切片: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 到这里)

这里的区别在于,

*=+= 类似: *= -> __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()

2.8 bisect

2.9 有些情况有比列表更好的选择

take away:

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 用来:

  1. documenting and formalizing the minimal interfaces for mappings
  2. 判断数据是否是 mapping 类型:from collections import abc; isinstance({}, abc.Mapping)

3.2 字典推导 dictcomp

3.3 mapping 类型的内置方法

值得注意的是

  1. d.update(m, [**kargs]),update 方法接受的 m 既可以是 mapping,也可以是键值对儿的迭代器(返回 (k, v) 对儿的迭代器)
  2. 有序字典可以用来实现,先进先出:OrderedDict.popitem(),后进先出:OrderedDict.popitem(last=False)
  3. 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 返回默认值:

  1. l = collections.defaultdict(list) # 把 list 构造方法作为 default_factory 来创建 defaultdict 注意: l['missing_k1'] 时才会调用 default_factory, l.get('missing_k2') 无效。
  2. 利用 __missing__ 自己处理缺失的 key

3.5 其他 mapping 类型

👆介绍了 dict 和 defaultdict,还有很多:

3.6 继承 UserDict 实现自己的 mapping(不要继承 dict)

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

# 快,且易读
found = len(needles & haystacks)`

# 慢
found = 0
for n in needles:
    if n in haystacks:
        found += 1

3.9 dict 和 set 背后的技术

4. Text and Bytes

decode 得到人类可读的文本, encode 成机器读的数字序列

4.1 basic && 4.2 bytes and bytearray

Py2 和 Py3 的区别:

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

4.4 编码问题

Q:那如何知道用了什么编码方式吗? A:理论上不可能,但是因为字节流可能有规律可以猜测出来(用 chardet

Q:BOM 是什么? A:Byte-order mark. 字节流开头的 b'\xff\xfe'就是 BOM,用来表示编码使用 Intel CPU 小字节序。

Q:为什么要区分大端小端? A:有一些码位需要多个字符来表示,比如 é 对应到 \xe9\x00,由于历史原因,有一些机器要把 \x00 放在前面,有一些放在后面,如何进行区分呢(字节序)?这时就要用到 BOM 字节序标记。

Q:什么时候不需要区分大小端? A:

  1. 一个字只占一个字符的时候(比如 ASCII)
  2. 生成的字节序列始终一致的编码,比如 UTF8

(注:尽管 UTF8 不需要 BOM,但是 Windows 的记事本仍然会在 UTF8 文件中添加 BOM(utf8编码下的 U+FEFF 是 b'\xef\xbb\xbf',所以看到一个文件如此开头,很可能是带 BOM 的 UTF8 编码文件。

Q:大端小端的区别是什么? A:小字节序机器:最低有效字节在前面,也就是 \xe9\x00;大字节序机器顺序正好相反,是 \x00\xe9

Q:如何区分大端小端? A:

  1. 直接指定用大端(utf_16be)还是小端(utf_16le)
  2. 没有指定的时候,在字节流的头部加上 BOM,\xff\xfe(255,254) 表示小端,\xfe\xff(254, 255)表达大端。
  3. 没有指定,也没有 BOM,按照标准应该假定为大端(用UTF-16BE编码),但是由于 Intel 使用小端,事实上很多没有BOM的文件用的是 UTF-16LE 编码

(补充,\xff\xfe 或者 \xfe\xff 叫做 ZERO WIDTH NOBREAK SPACE(U+FEFF),在UCS(通用编码集)中没有定义,专门用来区分大小端)

4.5 处理文本的最佳实践

4.6 要比较 unicode 字符串,要先进行规范化

4.7 Unicode 排序

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

主要有 reos

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

三、作为对象的函数

5. 函数是一等对象

整数、字符串、字典和函数都是一等对象。

一等对象的条件:

5.1 验证函数是一等对象

5.2 高阶函数

定义:接受函数做参数,或者返回值是函数的函数是高阶函数。

比如:

py2 py3:

5.3 lambda 匿名函数

限制:

5.4 可调用对象 Callable Object 有七种

符号

调用运算符 ()

可调用对象:

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 里去寻找这个参数? 通过自省

具体而言,要获取函数参数信息,需要在多个地方寻觅:

非常麻烦,所以直接用 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 库中的函数:

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

参考资料:

  1. Python函数内省如何理解?

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), 自由变量的名字 numsco_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 变成 局部变量。

要解决这个问题有两个方法:

  1. 显式声明 total 和 count 是自由变量,可以使用 Py3 的 nonlocal (1)
  2. 将不可变类型的数据放到可变类型里用作自由变量 (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>

注意:

  1. 2 用的 func 是 clock 的参数,也即 clock 函数体内的局部变量(对于 clocked 来说是自由变量)
  2. @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 标准库中的装饰器

装饰类方法的装饰器:

  1. property #19.2
  2. classmethod # 9.4
  3. 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 默认是浅拷贝(可能存在问题)

Python Tutor 的在线演示

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>  
  1. 给 l1 添加元素,对 l2 没有影响;
  2. 如果l1元素是可变的,因为浅拷贝只复制了元素的引用,导致对 l1 的修改同时会影响到 l2;
  3. 如果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 来实现深复制

如何实现浅拷贝?

  1. 对于内置的可变序列类型:直接使用内置的类型构造方法,比如 l2 = list(l1)l2 = l1[:]
  2. 其他对象使用 copy.copy: MyObj2 = copy.copy(MyObj)

如何实现深拷贝?

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

⚠️ 函数参数如果是可变类型的,要格外小心

  1. 参数默认值不要用可变对象!
  2. 如果定义的函数就是接收可变参数,那么在函数内对可变参数的修改是否要带到函数外部,需要函数的定义方要和函数的调用方达成共识。默认不要修改通过参数传入的对象!

8.5 del 和垃圾回收

  1. del 删除名称,del 不删除对象。
  2. del 如果删除了对象所有的引用,该对象可能会被 GC

8.6 弱引用

学习使用 weakref

# 以下在 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 中弱引用的所指对象(弱引用的目标)的限制:

take away 🌮

9. Pythonic 对象

自定义类型的行为可以像内置类型一样自然,依赖的是「鸭子类型」

9.1 对象表示形式

将对象表示成字符串:

用其他姿势表示对象:

9.2 以第一章的向量为例

目标,让 Vector 类实现以下功能 (v1 = Vector2d(3, 4)):

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

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__ 魔法方法。

9.6 让对象可散列 (hashable)

hashable 有几个要求:

  1. 实现 __hash__ 方法
  2. 实现 __eq__ 方法
  3. 不可变

9.7 私有属性只是君子协议

通过名称改写(name mangling)把双下划线开头的属性的名字改了

比如:Dog.__food -> Dog._Dog.__fod

9.8 用 __slots__ 节省内存

__slots__ 可以使用任何可迭代对象,最好用不可变的 tuple。

slots 注意事项⚠️:

  1. 就算父类有 __slots__,子类也要定义一下,因为 __slots__ 不会继承
  2. 实例中只有 __slots__ 中列出的属性,当然,可以把 __dict__ 放到 __slots__ 里(这样就不省内存了)
  3. __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. 用形如 [1, 2, 3] 的列表来初始化 Vector
  2. 维度信息保存在 array (数组)中
  3. 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)!

怎么解决?

  1. 用返回的 Array 再构造 Vector 类(不好,现在的 Vector 构造方法是接受一个 list,或者通过 frombytes 用二进制创建,并不支持通过 Array 创建)
  2. 直接在 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))

说明一下,

  1. 只允许单字母的 xyzt 这四个名字
  2. 其他情况抛出 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])
  1. 不应该允许对单个分量进行赋值,这里缺少限制
  2. 而当对分量赋值了之后,只修改了分量属性值,改动没有传递到 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>

有两点注意:

  1. 这里的生成器表达式可以换成 map:hashes = map(hash, self._components)(map 在 Py2 中效率较低,因为要构建一个列表)
  2. 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

注意两点:

  1. 在用 zip 逐个比较分量之前,一定要先比较长度 ⚠️,因为 zip 在一个输入先耗尽之时不会抛出异常
  2. 这里可以简化成:all(a == b for a, b in zip(self, other))

10.7 格式化输出的 Vector 第五版

除了笛卡尔坐标,这里还支持了球面坐标

take away

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 检查抽象基类?

11.5 定义抽象基类的子类

  1. 直接继承抽象基类即可 class FrenchDeck2(collections.MutableSequence):
  2. 要实现抽象基类中的抽象方法(即抽象基类并没有实现的方法,需要继承者自己实现)
  3. 抽象基类中已实现的具体方法可以直接继承来直接用(废话,就跟普通继承一样)
  4. 子类根据自己的情况可以替换抽象基类中的方法,使用更高效的自己的实现

11.6 看看标准库里有什么抽象基类

标准库里有两个 abc 模块:

  1. collections.abc (我们主要用这个)
  2. abc.ABC (用来创建新的抽象基类,见 11.7 的例子)

其他抽象基类:

  1. numbers:比如 isinstance(x, numbers.Integral)

11.7 自己创建一个抽象基类(只是了解工作原理,不要自己这么干!)

take away

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 多重继承最佳实践

  1. 把接口继承和实现继承区分开
  2. 使用抽象基类显式表示接口
  3. 通过混入重用代码
  4. 在名称中明确指明混入
  5. 抽象基类可以作为混入,反过来则不成立
  6. 不要子类化多个具体类
  7. 为用户提供聚合类
  8. 「优先使用对象组合,而不是类继承」

12.5 多重继承的例子二:Django

与 Tkinter 相比, Django 基于类的视图 API 是多重继承更好的示例。尤其是,入类易于理解:各个混入的目的明确,而且名称的后缀都是 …Mixin 。 需要新功能时,很多 Django 程序员依然选择编写单块视图函数,负责处理所有事务,而不尝试重用基视图和混入。

take away

13. 运算符重载

运算符重载的作用是让用户定义的对象使用中缀运算符(如 +| )或一元运算符(如 -~ )。

所以可以解决一个问题:

Vector2d.__eq__ 存在一个 bug, assert Vector(3, 4) == [3, 4],这不合理。

13.1 Python 中的运算符重载的限制

  1. 不能重载内置类型的运算符
  2. 不能新建运算符,只能重载已有的
  3. 逻辑运算符 is and not or 不可以重载

13.2 一元运算符

文档

  1. 取负 - __neg__
  2. 取正 + __pos__
  3. 取反 ~ __invert__
  4. 取绝对值 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__ 有一些区别:

  1. __eq__ 的反向操作符仍然是 __eq__(参数对调了);正向的 __gt__ 换成反向的 __lt__,参数也要对调。
  2. ==!=如果连反向调用都失败了,不会抛 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 增量赋值的 +=*=

  1. 没有实现就地操作符(比如 __iadd__),a+=b 只是 a = a + b 的语法糖。这时候不需要其他代码,只要有 __add__ 就够了。
  2. 如果要实现就地操作,就要实现 __iadd__
  3. 不可变类型一定不能实现就地操作特殊方法。(比如 tuple)

take away

五、控制流程

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 的作用:

  1. 检查是否有 __iter__ 方法,有则调用来创建迭代器
  2. 否则,检查是否有 __getitem__ 方法,有则创建迭代器,尝试从 0 开始按顺序取值
  3. 否则 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

有两个方法:

  1. __next__,返回下一个可用元素,元素耗尽就 raise StopIteration
  2. __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 等

  1. 用于过滤,产出输入的可迭代对象的子集
  2. 映射,map
  3. 合并,chain
  4. 拓展,产出比输入还多的元素
  5. 重新排列,groupby,反转(reverse)

14.10 yield from

Python 3.3 之后的版本才有。

yield from 有两个功能:

  1. 语法糖,代替一层 for 循环
  2. 见第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

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

上下文管理器协议:

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

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

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:

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 的调用过程分为三步:

  1. next(my_coro2): 第一个 print ,yield a
  2. my_coro2.send(28):send 的参数 28 赋值给 b,第二个 print,yield a + b,
  3. 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

存在的陷阱:

  1. 本装饰器和 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. 用 throwclose 方法显式地把异常传给协程 throw 可以传入任何异常,如果协程不能 handle 这种异常,异常就会向上传递,到达调用方这里

close 会在协程内暂停的 yield 表达式这里抛出 GeneratorExit:

  1. 如果生成器没有处理 GeneratorExit 或者抛出了 StopIteration(比如执行到协程末尾了),不会有异常传递出去
  2. 收到 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 终止。

名词解释:

16.8 yield from 的原理(意义)

16.9 用协程做离线事件仿真

出租车队仿真的例子,感觉很像 Effective Python 中生命游戏?

take away

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.Futureasyncio.Future

Future 的内置方法

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 的并发脚本都不能并行下载:

  1. concurrent.futures 版本受 GIL 限制
  2. 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 组合来得灵活:

  1. submit 可以接受不同的可调用对象和参数(map 只能处理同一个可调用对象,每次传入不同的参数)
  2. as_completed 接受各种类型的 Executor 实例,比如一些来自 ThreadPoolExecutor 一些来自 ProcessPoolExecutor

take away

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() 方法的行为不同:

既然结合 yield from 使用,asyncio.Future 一般不用 my_future.add_done_callbackmy_future.result

Task

得到 Task:

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 怎么用?

  1. 使用 yield from 串联起来的多个协程最外面必须由客户端代码(不是协程)驱动,客户端显式或隐式(比如 for 循环)在最外层的委派生成器上调用 next()send() 方法
  2. yield from 链条最内层的子生成器必须是简单生成器(只用 yield)或者可迭代对象
  3. 用 asyncio 包时,最外层委派生成器传给 asyncio API 的函数(比如 loop.run_until_complete())进行驱动
  4. 用 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. 把所有阻塞型操作都丢到各自独立的线程中
  2. 把阻塞型操作转换成非阻塞的异步调用

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

import asyncio
def run_sync(coro_or_future):
    loop = asyncio.get_event_loop()
    return loop.run_until_complete(coro_or_future)

#Python #bookreview