流畅的Python

第1章 Python 数据结构

一摞 Python 风格的纸牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]

上面这个代码示例惊艳到我了,让我对 Python 的类刮目相看;此刻我才开始开始意识到内置方法的存在;

例如它仅仅因为实现了 _len_ 和 _getitem_ 两个特殊方法,便使得这个类能够自动使用 Python 的内置函数,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 使用 len 函数获得数量
deck = FrenchDeck()
len(deck)

# 使用索引访问列表中的元素
deck[0]
deck[-1]

# 使用内置的标准库,例如 random,从列表中随机读取元素
from random import choice
choice(deck)

# 自动支持切片操作
deck[:3]
deck[12::13]

# 自动可迭代
for card in deck:
print(card)

# 自动支持 in 运算符
Card("Q", "hearts") in deck

# 只需定义排序规则,即可自动支持内置的 sorted 排序函数
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
rank_value = FrenchDeck.rands.index(card.rand)
return rank_value * len(suit_values) + suite_values[card.suit]

for card in sorted(deck, key = spades_high):
print(card)

如何使用特殊方法

特殊方法是为了给解释器调用,从而实现一些内置的功能,而不是为了自己调用;如果是自己调用,那么只需写普通方法即可,无须写特殊方法;

另外,也尽量避免随意添加特殊方法,因为有可能出解释器内置的方法出现命名冲突,导致发生不可预知的情况;

特殊方法还可以用来重载运算符,例如转成字符串,加号,乘号,取绝对值等,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from math import hypot

class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y

# repr 用来定义对象用字符串如何显示,另外还有一个 str 用来给 str() 或者 print 函数调用
# 通常定义 repr 即可,它更加通用
def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)

def __abs__(self): # 重载了 abs 函数
return hypot(self.x, self.y)

def __bool__(self): # 当调用 bool 函数时,如何判断对象是否为真
return bool(self.x or self.y)

def __add__(self, other): # 重载了加号
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, scalar): # 重载了乘号
return Vector(self.x * scalar, self.y * scalar)

特殊方法一览

特殊方法挺多的,有80 多个,其中有 40 个多用于实现算术运算、位运算和比较操作;

为什么 len 不是普通 方法

len 的目的是为了读取对象的长度,对于内置类型的对象,它们是用 C 语言的 struct 表示的,struct 里面有个属性存储着长度值,因此在这种情况下,len 会直接去读取 struct 的长度值,而不是调用 _len_ 来计算长度;主要是出于性能考量

本章小结

通过实现特殊方法,能够让自定义类型表现跟内置类型一样,从而能够直接使用 Python 的很多内置函数,让代码更容易阅读;

第2章 序列构成的数组

内置序列类型

Python 有两种序列类型,一种存放的是对象的引用,因此它可以容纳任何类型,称为容器序列;一种存放值,而不是引用,因此只能放相同类型的值,称为扁平序列;

序列按照能否修改,可分为可变序列和不可变序列

  • 可变序列(Mutable Sequence):list, bytearray, array.array, collections.deque, memoryview
  • 不可变序列(Sequence):tuple, str, bytes

列表推导和生成器表达式

列表推导式(list comprehension)非常适合用来创建新的列表,这种写法更容易读懂;如果列表推导太长,则可以改用传统的 for 循环;

1
2
3
4
5
6
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]

生成器表达式用来其他类型的序列;生成器表达式使用圆括号,而不是方括号;

1
2
3
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) # 由于生成器表达式是函数的唯一参数,所以无需用括号括起来
(36, 162, 163, 165, 8364, 164)
1
2
3
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) # 非唯一参数,所以多加了一层括号
array('I', [36, 162, 163, 165, 8364, 164])

生成器表达式每次产生一个运算结果,而不是一下生成整个列表,这样可以节省内存,尤其是元素多的时候,非常明显

1
2
3
4
5
6
7
8
9
10
11
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): # 一次只生成一个计算结果,而非整个列表
... print(tshirt)
...
black S
black M
black L
white S
white M
white L

元组不仅仅是不可变的列表

元组是不可变列表,但其实它存放的数据,也可以基于顺序来表达不同的含义

1
2
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
# 位置1是国家,位置2是代号
1
2
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # 元组拆包
1
2
>>> b, a = a, b
# 使用拆包,实现变量的值交换
1
2
3
4
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t) # 星号 * 可用来将元组拆包成函数的函数

星号* 可用来存放拆包的余下元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> a, b, *rest = range(5)  # 星号 * 用来存放剩余元素
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])

# 放在中间的位置也可以
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)

# 放在开头的位置也可以
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)

嵌套元组拆包

1
2
3
4
5
6
7
8
9
10
11
metro_areas = [
('Tokyo','JP',36.933,(35.689722,139.691667)), # 嵌套的元组
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

for name, cc, pop, (latitude, longitude) in metro_areas: # 嵌套拆包
if longitude <= 0:
print(fmt.format(name, latitude, longitude))

具名元组

1
2
3
4
5
6
7
8
9
10
11
12
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates') # 定义元组结构
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) # 赋值
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667)) # 各个
>>> tokyo.population ➌
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'

具名元组有一些内置的属性和方法,包括:

  • _fields 属性,用来查看所有字段的名称
  • _make() 方法,用来创建实例
  • _asdict() 方法,用来返回 OrderDict
1
2
3
4
5
6
7
>>> City._fields 
('name', 'country', 'population', 'coordinates')
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
>>> delhi = City._make(delhi_data)
>>> delhi._asdict()
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])

相对列表,元组没有添加和删除元素的方法,其他方法则都差不多;

切片

切片有个特殊的用法,即 s[a : b : c],它表示在 a ~ b 的区间内,以 c 为间隔取值;即 s[start : stop : step]

1
2
3
4
5
6
7
8
9
>>> s = 'bicycle'
>>> s[::3] # 正序,间隔 3 取值
'bye'
>>> s[::-1] # 倒序,间隔 1 取值
'elcycib'
>>> s[::-2] # 倒序,间隔 2 取值
'eccb'

>>> deck[12::13] # 正序,从 12 开始,间隔 13 取值,

切片有个很有意思的用法,它可以让代码更易读

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY = slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)

>>> for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION]) # 此处的 UNIT_PRICE 也可硬编码,但这样写更优雅
...
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240

切片也可用来赋值,或者删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30] # 赋值
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7] # 删除
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22] # 赋值
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 # 不可行,右侧需要是可迭代对象,不能是数值
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100] # 可行
>>> l
[0, 1, 100, 22, 9]

对序列使用 + 和 *

加号 + 用来表示将两个序列拼接起来,并返回一个新的序列;

乘号 * 表示重复多份序列并拼接起来

1
2
3
4
5
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'

特别注意,在 [a] * n 这个表达式中,如果 a 是一个引用,那么复制出来的是 n 个引用,并且这 n 个引用实际上指向同一个对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 正确用法,使用列表推导式
>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

# 错误用法
>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']] # 虽然有三个列表,但指向同一个对象

序列的增量赋值

自增 += 或者自乘 *= 实际上调用的是 _iadd_ 和 _imul_ 方法,如果一个类没有实际 iadd 方法,那么解释器就会调用 add 方法来计算,此时相当于 a = a + b,因此,如果 a + b 返回的是一个新的对象,那么 a 将指向该新的对象,而不是改变旧对象的值;

1
2
3
4
5
6
7
8
>>> l = [1, 2, 3]
>>> id(l)
4311953800
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800

元组是不可变的,当在元组里面放入一个可变序列时,会出现异常情况,即该可变序列可被改变,但是无法将改变后的新序列,赋值给元组的引用;

1
2
3
4
5
6
7
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment # 赋值给 t[2] 的时候报错了
>>> t
(1, 2, [30, 40, 50, 60]) # 成功改变了序列

list.sort 方法和内置函数 sorted

list.sort 会就地修改列表,返回 None

sorted 则不会修改原列表,而是会返回一个新的列表;

用 bisect 来管理已排序的序列

bisect 用来从有序列表中查找某个值的插入位置,满足插入后原序列的顺序不变;

insort 用来将元素插入到有序列表中,插入后顺序保持不变;

当列表不是首选时

数组

array.array:数组里面存储的不是对象,而是字面值(例如数字,在内存中直接用字节表示即可);因此它的读定性能要高很多;但因此它能够存储的类型也比较有限,只有少数几种;

创建数组时,需要通过参数指定类型,以便解释器能够决定如何分配内存空间;

内存视图

memory view

在不复制内容的情况下,操作数组的切片,例如 Numpy;

1
2
3
4
5
6
7
8
9
10
11
12
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')
>>> memv_oct.tolist()
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 # 此处的赋值,改变的是高位字节部分
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) # 原本的 0,因为高位字节改变,变成了 1024

Numpy 和 SciPy

操作高阶数组和矩阵的利器;

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import numpy ➊
>>> a = numpy.arange(12) ➋
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape ➌
(12,)
>>> a.shape = 3, 4
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])

双向队列和其他形式的队列

虽然可以用列表在模拟队列,但是性能并不好,尤其是在头部插入新元素时;双向队列更方便,而且可以指定长度,在超出长度时,会自动删除较早的内容;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) ➊
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3) # 旋转,将最后3个元素,放到前面来
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4) # 将头部 4 个元素,放到后面去
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1) # 添加到头部,会自动删除尾部溢出的部分
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) # 添加到尾部,会删除头部溢出的部分
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40]) # 逐一添加到头部,因此顺序会反过来
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

注:append 和 popleft 是原子操作,因此是线程安全的;

除了双向队列,还有以下几种队列,分别是:

  • queue:如果队列满了,不会自动删除旧元素,而是会被锁住;因此可用来控制活跃线程的数量;
  • multiprocessing:用于进程间的通信
  • asyncio:用于异步编程
  • heapq:堆队列

第3章 字典和集合

字典构造方法

如果一个对象是可散列的,那么它的散列值需要不可变,而且这个对象需要实现 hash 和 eq 方法,以便可以计算散列值并和其他对象做比较;

字典有很多种构造方法

1
2
3
4
5
6
7
>>> a = dict(one=1, two=2, three=3)
>>> b = {'one': 1, 'two': 2, 'three': 3}
>>> c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
>>> d = dict([('two', 2), ('one', 1), ('three', 3)])
>>> e = dict({'three': 3, 'one': 1, 'two': 2})
>>> a == b == c == d == e
True

字典推导

字典可以从任何以键值对作为元素的可迭代对象中构造出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> DIAL_CODES = [ ➊
... (86, 'China'),
... (91, 'India'),
... (1, 'United States'),
... (62, 'Indonesia'),
... (55, 'Brazil'),
... (92, 'Pakistan'),
... (880, 'Bangladesh'),
... (234, 'Nigeria'),
... (7, 'Russia'),
... (81, 'Japan'),
... ]
>>> country_code = {country: code for code, country in DIAL_CODES} # 构造1 country : code
>>> country_code
{'China': 86, 'India': 91, 'Bangladesh': 880, 'United States': 1,
'Pakistan': 92, 'Japan': 81, 'Russia': 7, 'Brazil': 55, 'Nigeria':
234, 'Indonesia': 62}
>>> {code: country.upper() for country, code in country_code.items() # 构造2 code : country
... if code < 66}
{1: 'UNITED STATES', 55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA'}

常见的映射方法

有个 setdefault 方法不常用,但其实很不错。它的用法如下

1
2
3
4
>>> a = {"abc": 123}
>>> b = a.setdefault("abc", 456) # 如果 abc 没值,则赋值456;如果有值,则返回值
>>> b
123

映射的弹性键查询

通常情况下,当我们使用 dict[key] 的方式访问时,如果该 key 不存在,会出现报错;而 collencts.defaultdict 可以处理这种情况;它会将该键为一个预先设定好的默认值,并返回该值

1
2
3
4
5
6
7
8
9
10
11
12
import sys
import re
import collections
WORD_RE = re.compile(r'\w+')
index = collections.defaultdict(list) # list 代表默认的构造方法,如键不存在,则会调用该构造方法,构造默认值
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start()+1
location = (line_no, column_no)
index[word].append(location)

defaultdict 仅在 dict[key] 下有效,在 dict.get(key) 是无效的,后者不会调用预设的工作方法;defaultdict 背后的工作原理是因为实现了 _missing_ 方法;当 _getitem_ 找不到键名时,默认会调用 missing 方法;因此,只要有实现该方法,即可以实现默认值的初始化和返回;

考虑到 missing 会被调用,那么就可以在这里设置手脚;例如将键名由数值转换字符串,以支持不管传入哪种类型,都可以找到对应的键;

字典的变种

collections.OrderDict

会记录每个键的添加顺序,然后可以删除最晚或者晚早添加的键;

collections.ChainMap

ChainMap 会将多个 dict 组合成一个 chain,让它表现起来,像是一个 dict

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> baseline = {'music': 'bach', 'art': 'rembrandt'}
>>> adjustments = {'art': 'van gogh', 'opera': 'carmen'}
>>> cm = ChainMap(baseline, adjustments)
>>> cm
ChainMap({'music': 'bach', 'art': 'rembrandt'}, {'art': 'van gogh', 'opera': 'carmen'})
>>> list(cm)
['art', 'opera', 'music']
>>> cm['music']
'bach'
>>> cm['art']
'rembrandt'
>>> cm.values()
ValuesView(ChainMap({'music': 'bach', 'art': 'rembrandt'}, {'art': 'van gogh', 'opera': 'carmen'}))

collections.Counter

1
2
3
4
5
6
7
8
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) # 计算每个键的出现次数
>>> ct.update('aaaaazzz') # update 会递增键的出现次数
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(2) # 可以返回最常见的 n 个键,此处是最常见的 2 个键
[('a', 10), ('z', 3)]
1
2
3
4
5
>>> cnt = Counter()
>>> for word in ['red', 'blue', 'red', 'green', 'blue', 'blue']:
>>> cnt[word] += 1
>>> cnt
Counter({'blue': 3, 'red': 2, 'green': 1})

collections.UserDict

用于让用户继承来编写子类,与 dict 的不同之处在于它是纯 Python 实现;而 dict 为了性能,某些功能的实现并不完全按照规范;

子类化 UserDict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import collections

# 实现 dict[key] 不管 key 是字符串还是数字,都可以正常访问
class StrKeyDict(collections.UserDict):

def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]

def __contains__(self, key):
return str(key) in self.data

def __setitem__(self, key, item):
self.data[str(key)] = item

一些好用的方法

update

1
2
3
4
5
6
>>> td1 = {'name': 'Zara', 'age': 7}
>>> td2 = {'sex': 'female'}
>>> td1.update(td2)
>>> td1
{'name': 'Zara', 'age': 7, 'sex': 'female'}
>>>

不可变映射类型

Python 的标准库并不支持不可变映射类型,但是有个变通的办法来实现相同的效果,即通过 MappingProxyType,从名字可以看得出来它是一个代理,这个代理是只读的;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from types import MappingProxyType
>>> d = {1:'A'}
>>> d_proxy = MappingProxyType(d) # 创建一个代理

>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1] # 代理是可以访问的
'A'

>>> d_proxy[2] = 'x' # 但是不可以修改,会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment

>>> d[2] = 'B'
>>> d_proxy # 代理可以实时的看到 d 更新后的效果
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'

集合论

集合 set 是一些对象的集合,它可以用来去重;集合中的元素必须是可散列的;

1
2
3
4
5
>>> l = ['spam', 'spam', 'eggs', 'spam']
>>> set(l)
{'eggs', 'spam'}
>>> list(set(l))
['eggs', 'spam']

集合有一些自己的运算符,以便计算合集、交集、差集等;

1
2
# 用 & 符号求交集
found = len(needles & haystack)

集合字面量

创建空集需要使用 set(), 而不是 { },不然就变成了字典了;

s1 = {1, 2, 3} 的性能比 s2 = set([1, 2, ,3]),因为后者涉及先构造列表的动作;

集合推导

集合推导和列表推导的唯一差别在于方括号 [ ] 还是花括号 { }

1
2
3
4
>>> from unicodedata import name ➊
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} ➋
{'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}

集合的操作

集合有不少专属的操作,这些操作很多是通过对运算符的重载来实现的;

dict 和 set 背后

dict 和 set 背后的实现原理是散列,这样性能就不会因为元素数量的增长出现大多波动;散列本质上是以空间换时间;列表则是以时间换空间;

dict 的实现及其结果

注:所有由用户自定义的对象,都是可散列的;因为它的散列值是由 id() 来生成的,跟对象本身的值没有关系。因此所有这些自定义对象,即使值相同,由于 id 不同,它们也是不相等的;

相比列表,元组会比较节省空间;一方面是因为它无须重复存储键名,另一方面是它不需要用到散列;

应避免在迭代的过程中,对字典进行修改,它会给迭代带来扰乱,有可能导致出错,或者结果错乱;

字典 dict 是不可散列的,所以无法直接将 dict 添加到 set 中;

字典 dict 的键名顺序是有可能会变化的,例如当出现散列冲突时或者扩容时;

第4章 文本和字节序列

字符问题

在 Python3,字符统一使用 Unicode 进行表示(称为码位),这样能够涵盖所有的已知字符,而且这个字符的 Unicode 也是固定的;但是在存储的时候,可以有多种编码方法(将码位转成字节序列),例如 UTF8, UTF16 等;使用不同的编码方法存储,就需要使用相应的解码方法读取,这样出来的结果才是正确的;

字节概要

在 Python3,有 bytes 和 bytearray 两种字节序列类型,其内部的元素是 0~255 的整数;

  • bytes[0],返回一个元素;
  • bytes[:1],返回一个切片,即一段新的序列

虽然二进制序列在底层是整数序列,但是显示的字面量有多种可能,包括:

  • ASCII 字符
  • 制表符、换行符、回车符、斜杠等特殊符号;
  • 十六进制转义表示
1
b'caf\xc3\xa9'  # caf 刚好可以用 Ascii  表示,后来两个只能用十六进制表示

基本的编解码器

了解编解码问题

处理文本文件

为了正确比较而规范化 Unicode 字符串

Unicode 文本排序

Unicode 数据库

支持字符串和字节序列的双模式 API

第5章 一等函数

把函数视为对象

first class 函数满足以下条件:

  • 能够在运行时创建
  • 能够赋值给变量或者数据结构中的元素;
  • 能够做为参数传递给函数;
  • 能够做为结果从函数调用中返回;

简而言之,函数就像一个对象一样(事实上在底层实现也是如此,函数即对象);

高阶函数

高阶函数:higher-order function,接受函数做为参数,或者返回结果为参数;

常用的 map 和 filter,可以用列表推导式和生成器表达式进行替代,看起来更容易理解,示例如下:

1
2
3
4
5
map(func, range(6))
[func(i) for i in range(6)]

filter(lambda n : n % 2, range(6))
[i for i in range(6) if i % 2]

匿名函数

由于 python 的 lambda 函名函数只能写单行的表达式,因此表达能力非常有限,导致使用场景非常少;常用于高阶函数的函数参数;类似下面这个样子

1
filter(lambda n : n % 2, range(6))

可调用对象

调用运算符,即一对括号,不仅可以运用在函数上,其实也可以运用在普通对象上;

可用 callable 函数来判断某个对象是否可以调用

用户定义的可调用类型

事实上所有对象都是可以调用的,只要对象有实现 call 方法即可;

函数内省

函数内省,function introspection,这个翻译名称有点奇怪;

由于函数是一个对象,因此其实这个对象内部存储着很多与函数有关的信息,示例如下:

从定位参数到仅限关键字参数

python 的函数参数处理机制非常灵活强大,既支持固定位置的参数形式,也支持按关键字进行匹配的参数形式。同时还支持使用 * 单星号或者 ** 双星号,将不固定数量的任意个参数,打包成一个可迭代对象,以便在函数体内部进行访问;其中单个星号打包成 tuple 元组的形式;两个星号打包成字典 dict 的形式;

获取关于参数的信息

函数内部的属性,可用来做一起有用的事情,示例如下:

此处使用了装饰器,装饰器会检查 hello 函数内部属性中存储的与参数有关的信息。检查后,它会发现 hello 函数需要一个 person 函数;因此,它可以用 query 对象中,获取相应的 person 值,然后作为参数,传递给 hello 函数;

_default_ 存储函数参数的默认值;

_code_ 是一个对象,它也存储着函数的相关信息,例如:

  • co_varnames 存储着参数名称 + 局部变量名称
  • co_argcount 存储着函数的参数个数;

直接访问 code 对象或者 default 不是很方便,不过有个 inspect 库提供了方便的查看方式;

函数注解

函数注解可用来给参数和返回值备注类型

1
2
def clip(text:str, max_len:'int > 0'=80) -> str: # 备注参数和返回值的类型
"""省略"""

注解会存储在函数的 _annotations_ 属性中;

注解本身不会做任何事情,有注解跟没有注解是一样的;但是注解可以给第三方工具(例如框架、装饰器等)提供有用的信息,例如 IDE 或者 Lint 工具可以利用注解来检查;

支持函数式编程的包

operator 模块

operator 模块提供了一些算术运算符函数,它让代码更加简单易懂;

1
2
3
4
5
from functools import reduce
from operator import mul

def fact(n):
return reduce(mul, range(1, n+1)) # mul 函数可用来计算两个数值的乘积

itemgetter 可用来读取元组中的元素

1
2
3
4
5
6
7
8
9
10
11
>>> metro_data = [
... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
... ]
>>>
>>> from operator import itemgetter
>>> for city in sorted(metro_data, key=itemgetter(1)): # itemgetter(1) 等同于 lamba fields : fields[1]
... print(city)
1
2
3
4
5
6
7
8
9
10
# 此处的 itemgetter 的两个参数,表示读取两个位置的值,组成元组
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')

attrgetter 与 itemgetter 的不同之处在于它使用名称来提取对象的属性;

methodcaller 接受一个参数,表示要调用的函数名称,然后它可以在之后传入的对象中调用相应的方法;

1
2
3
4
5
6
7
8
9
>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper') # 表示调用 upper 方法
>>> upcase(s) # 在 s 身上调用 upper 方法
'THE TIME HAS COME'

>>> hiphenate = methodcaller('replace', ' ', '-') # 调用 replace 方法
>>> hiphenate(s) # 在 s 身上调用
'The-time-has-come'

使用 functools.partial 冻结参数

partial 可用来将某个函数的参数设置为固定值

1
2
3
4
5
6
7
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) # mul 原本接受两个参数,此处将 mul 的第一个参数固定 3
>>> triple(7) # 调用时,只需传入第二个参数即可计算出结果
21
>>> list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]

第6章 使用一等函数实现设计模式

策略模式

在函数作为一等公民时,很多设计模式就有了更简单的实现方法了;例如策略模式中,每个策略对应一个类;实际上它们都可以简单替换成函数即可,完全没有必要单独为了调用它而去实例化一个对象;

命令模式

命令模式的本意是想在命令的调用者(操作对象)和接收者(实现对象)之间进行解耦,这样调用者无须了解各个接收者具体是什么接口,而让它们对接口进行统一命名;但其实有更简单的做法,即直接将各个实现绑定到调用者身上就可以了,有点像回调那样;

面向对象之所以要搞成那么复杂,完全是因为它们不能接受函数作为参数,而是只能接受对象做为参数,然后再去调用对象的方法,这样就不得不对所调用的方法有个规范命名,不然就不知道如何调用;但如果能够接受函数作为参数,那就完全不一样了,直接将形参当作函数调用即可,非常简单直观,容易理解;

第7章 函数装饰器和闭包

装饰器基础知识

装饰器是一个可调用的对象,类似函数,它的参数是另外一个函数,它的目的是对该函数进行打包封装,干些额外的工作;它的执行结果有可能会返回参数函数,也有可能是返回另外一个新的函数或可调用对象,并赋值给原本作为参数的函数名称,这样调用者并不知道这个函数可能已经被替换了;

1
2
3
4
5
6
7
8
9
@decorate
def target():
print("running target()")

# 上面的写法跟下面的写法是一个意思
def target():
print("running target()")

target = decorate(target);

Python 何时执行装饰器

注意,在定义装饰器的代码文件被加载时,装饰器会被立即执行,此时被装饰的函数还没有被调用;

使用装饰器改进策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
promos = [] 

def promotion(promo_func):
promos.append(promo_func)
return promo_func

@promotion # 使用装饰器,在添加新的折扣策略时,不容易遗漏
def fidelity(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion

@promotion
def bulk_item(order):
"""单个商品为20个或以上时提供10%折扣"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount

变量作用域规则

python 在编译函数定义时,它会先检查函数中声明的局部变量;

  • 如果变量存在,那么之后使用变量时,解释器只会在本地作用域中寻找;
  • 如果不存在,那么就会到函数的定义环境中寻找全局变量;

global 关键字可用来告知某个变量为全局的,以引导解释器到正确的位置查找

1
2
3
4
5
6
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9

闭包

如果函数引用了某个变量,该不在其定义内部定义,而是在函数外部定义的,那么解释器会在函数对象中,保留一个指向该外部变量的引用,以便在使用该变量时,能够取到相应的值;

1
2
3
4
5
6
7
8
def make_averager():
series = []
def averager(new_value):
# 此处引用的 series 变量在 averager 外部定义,averager 对象属性中会保存它的引用
series.append(new_value)
total = sum(series)
return total/len(series)
return averager

nonlocal 声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
# 此处的表达式等同于 count = count + 1,因此解释器会将 count 当作局部变量
# 因此在执行 count + 1 会出现报错
total += new_value
return total / count
return averager

# 为了解决以上问题,需要用 nonlocal 关键字将 count 和 total 声明为非局部变量
def make_averager():
count = 0
total = 0

def averager(new_value):
nonlocal count, total # 声明 nonlocal
count += 1
total += new_value
return total / count

return averager

实现一个简单的装饰器

1
2
3
4
5
6
7
8
9
10
11
import time

def clock(func):
def clocked(*args): # 不支持关键字参数
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ", ".join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return clocked

支持关键字参数的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
import functools

def clock(func):
@functools.wraps(func) # 用于将函数属性从 func 复制到 clocked 函数中,例如函数名称等
def clocked(*args, **kwargs): # 支持关键字参数
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(", ".join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(", ".join(pairs))
arg_str = ", ".join(arg_lst)
print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
return clocked

标准库中的装饰器

使用 lru_cache 缓存

lru_cache 可以帮助缓存函数的计算结果,如果下次再传入相同的参数,则直接从缓存中返回计算结果,不再重复计算,这会极大的提高性能,尤其是存在大量重复计算的场景,例如计算斐波契那数列;

lru 的全称 least recently used

单分派泛函数

所谓的分泛函数是指这个函数的功能用于分别派分任务,它根据参数值,使用一串 if elif else 来分别调用相应的函数;在 OO 的语言中一般叫重载,但 Python 不支持重载;

singledispatch 装饰器,可以将多个函数组合成一个泛函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch # 将 htmlize 包装成了泛函数,之后它可以注册不同的参数
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)

@htmlize.register(str) # 注册重载 str 类型
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral) # 注册重载 int 类型
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple) # 注册 tuple 类型
@htmlize.register(abc.MutableSequence) # 可多个类型叠加
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'

singledispatch 可以用来装饰自己编写的函数,也可以用来装饰他人编写的函数;

叠放装饰器

装饰器支持叠放

1
2
3
4
5
6
7
@d1
@d2
def f():
print("f")

#等同于
f = d1(d2(f))

参数化装饰器

通过创建一个装饰器工厂函数,便可使装饰器支持传入参数;调用该装饰器工厂函数时,返回的是真正的装饰器;

第8章 对象引用、可变性和垃圾回收

变量不是盒子

变量本身是一个独立的东西,我们借助它,让它指向某个对象,以方便实现引用该对象;

标识、相等性和别名

在 Python 中,判断两个对象是否相同,有两种方法,一种是 == 两个等号,一种是使用关键字 is,它们的意思是不一样的;== 会调用对象的 __eq __ 方法进行判断,它比的是值相等即可,is 等是判断对象的 id,相当于内存的地址;

由于 is 比较的是地址,因为使用 is 进行判断它的性能很好;因为使用 == 进行判断的话,需要遍历对象的属性值;

object 类型的 eq 方法比较的是 id,但是其他大多数内置类型的 eq 方法比较的是值;

当元组用于保存对象时,它保存的是对象的引用。虽然元组本身不可变,但这个引用背后的对象自身是可以变的;

默认做浅复制

如果要做深复制,需要使用 deepcopy 方法;浅复制则使用 copy 方法;

函数的参数作为引用时

千万不要将函数参数的默认值设置为可变对象,而应该设置为 None;因为如果是可变对象,那么在函数载入时,会自动创建出来;这样导致多次不传参数的调用该函数时,多个函数都会指向该默认值,造成相互影响;

1
2
3
4
5
6
7
8
9
10
class HauntedBus:

def __init__(self, passengers=[]): # 这里默认值 [] 是大忌,千万要避免
self.passengers = passengers

def pick(self, name):
self.passengers.append(name)

def drop(self, name):
self.passengers.remove(name)

如果函数的参数是一个可变对象,那么让函数对该对象进行修改,会直接作用到外部的实参对象上。有时候,这是想要的结果,有时候则不是非预期的结果。如果是非预期的结果,那么函数内部应对该实参进行复制;

del 和垃圾回收

del 关键字并不是用来销毁对象的,而仅仅是切割变量和对象之间的引用关系;当对象的引用数量为零时,销毁的工作会垃圾回收器处理;

弱引用

弱引用不会增加对象的引用计数,这样不会对对象的垃圾回收带来干扰;一般用于有生命周期限制的缓存管理中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> import weakref

>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) ➊

>>> wref
<weakref at 0x100637598; to 'set' at 0x100636748>

>>> wref() ➋
{0, 1}

>>> a_set = {2, 3, 4} ➌
>>> wref() ➍
{0, 1}

>>> wref() is None
False
>>> wref() is None
True

WeakValueDictionary 是一种可变映射(字典也是一种可变映射),映射指向的值是对象的弱引用;当对象被回收时,对应的键会自动从 WeakValueDictionary 中被删除;因此,它很适合用来做缓存;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind

>>> import weakref
>>> stock = weakref.WeakValueDictionary() # 实例化
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
... Cheese('Brie'), Cheese('Parmesan')]
...
>>> for cheese in catalog:
... stock[cheese.kind] = cheese # 将 stock 的键映射到 cheese 实例上
...
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] ➌
>>> del catalog
>>> sorted(stock.keys())
['Parmesan'] # 为什么删除 catalog 后,没有全部删除,而是还剩下一个?
>>> del cheese # for 循环中的 cheese 是全局变量,因此需要显式删除,不然仍然有一个引用
>>> sorted(stock.keys())
[]

不是每个 python 对象都可以被弱引用,例如常用的 list 和 dict 实例无法被弱引用,但是它们的子类可以;set 实例也可以

1
2
3
4
5
6
7
class MyList(list):
"""list的子类,实例可以作为弱引用的目标"""

a_list = MyList(range(10))

# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list)

Python 对不可变类型施加的把戏

使用一个元组构建另外一个元组,结果得到的是同一个元组

1
2
3
4
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1
True

在 CPython 中,当对象的引用数量为零,会立即触发垃圾回收。但其他 Python 实现则不一定如此;这里涉及到性能的权衡;

第9章 符合 Python 风格的对象

对象表示形式

Python 默认使用两个函数来表示对象的字符串形式,它们分别是 repr() 和 str() 函数。它们实际上调用的是对象的 _repr_ 和 _str_

另外还有一个 bytes() 函数会调用 _bytes_ 方法来返回字节序列;

再谈向量类

以下是自定义向量类的待实现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y) # 能够通过点运算符,直接访问属性
3.0 4.0
>>> x, y = v1 # 支持元组拆包
>>> x, y
(3.0, 4.0)
>>> v1 # repr 的显示格式
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1)) # 基于 repr 结果生成对象
>>> v1 == v1_clone # 支持 == 运算符
True
>>> print(v1) # str 的实现
(3.0, 4.0)
>>> octets = bytes(v1) # 生成实例的二进制表示
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1) # 支持 abs 方法,返回实例的模
5.0
>>> bool(v1), bool(Vector2d(0, 0)) # 支持 bool 方法,模为零时,返回 False
(True, False)

备选构造方法

classmethod 与 staticmethod

classmethod 修饰的函数,调用时,不需要实例化对象;该函数的第一个参数是类本身,从而可以借助该函数,访问类的相关成员;

classmethod 的一个常见用途时定义额外的构造方法,一般该构造方法会对传入的数据进行清洗,之后再构造对象;

1
2
3
4
5
@classmethod
def frombytes(cls, octets): # 第一个参数不是 self, 而是类本身
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)

格式化显示

1
2
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl) # rate 表示具名变量
'1 BRL = 0.41 USD'

关于如何格式化,python 有一套自己的语法规则,可称之为微语言;这套微语言是可扩展的,可以自定义如何解释 forma_spec 参数

可散列的 Vector2d

通过实现 _hash_ 和 _eq_ 方法,可将一个不可散列的自定义类的对象,变成可散列的;

Python 的私有属性和受保护的属性

python 没有类似 Java 中的 private 关键字,而是通过给类成员的名称添加两个下划线前缀,将该成员标记为私有成员,类似这样:__x,但也有一些人喜欢使用一个下划线来表示;

对于私有属性,解释器在实例化对象时,会给这些属性加上类名作为前缀,这样一来,直接用双下划线访问私有属性时,会提示该属性并不存在,从而实现访问控制;但实际上是可以访问的,只是曲折一点,需要加上类名前缀来访问;

使用 slots 类属性节省空间

默认情况下,类的实例在 _dict_ 字段中使用字典来存储属性成员,如果成员比较多的,会占据较大的内存,此时可考虑使用 _slot_ 属性来存储以节省内存;它的原理是使用元组来存储,所以节省内存;

1
2
3
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'

覆盖类属性

Python 的类属性可以为实例属性提供默认值,这个默认值在实例中可以被重新赋值;

如果要批量处理,则可以考虑定义一个子类,该子类的属性重写,之后使用子类来实例化对象;

第10章 序列的修改、散列和切片

鸭子类型:只要实现一些约定的接口,即可当作拥有目标类型的特征,并可以像目标类型一样被处理;例如一个类只需要实现 getitem 和 len 两个接口,那么它就可以被当作序列类型一样处理,至于它是谁的子类,并不重要;

zip 函数可用于并行迭代多个序列,它会将多个序列的对象打包成元组,然后可以拆包赋值给各个变量;

第11章 接口:从协议到抽象基类

接口与协议

一个类只需要实现了某些特定的接口,它就可以被当作特定的类型进行操作(即鸭子类型);

当一个类实现了 getitem 接口时,即使它没有实现 contains 和 iter 接口,它也是可迭代,并且支持 in 运算符的;因为解释器会调用 getitem 接口来实现以上两项功能;

Python 类中的方法,第一个参数叫 self 纯粹是一种惯例,其实叫个其他名字也无妨;

猴子补丁

猴子补丁:如果一个类在定义时,没有定义某个方法;之后在运行时,可以在外部单独定义一个函数,然后把这个函数绑定到类的某个属性上,这样就让类动态获得了某个方法;

抽象基类

抽象基类一般用于编写框架的场景,如果是业务场景,几乎不太可能需要自己编写抽象基类,而是使用现成的就可以了;

当继承抽象基类,就需要手工实现抽象基类中规定的所有方法,不管该方法是否用得到;

标准库中的 ABC

ABC:抽象基类,abstract base class

不可变集合:Sequence, Mapping, Set

可变集合:MutableSequence, MutableMapping, MutableSet

数字塔

numbers 包定义了数字抽象基类的线性层次结构:Number < Complex < Real < Rational < Integral;

第12章 继承的优缺点

子类化内置类型很麻烦

内置类型的方法不会调用子类覆盖后的方法,它只会调用内置类型原本的方法;因此,不会子类化内置类型,Python 有专门给用户子类化的类型,以 User 开头,例如 UserDict、UserList、UserString 等;

猜测原因在于内置类型的很多方法,出于性能考虑,是用 C 语言专门优化过的,因此不严格遵行继承的定义;

多重继承和方法解析顺序

多重继承会面临菱形问题,即子类继承多个父类中,存在同名的方法,导致子类无法确定应该执行哪个父类的同名方法;

多重继承的真实应用

处理多重继承

Django 示例

第13章 正确重载运算符

第14章 可迭代对象、迭代器和生成器

迭代器模式:惰性加载数据,处理时加载,这样可以用较小的内存,处理很大的数据集;

Python 中使用生成器来实现迭代器模式;生成器也是为了迭代数据,因此可将它当作迭代器来使用,唯一的区别在于它的惰性;

在 Python3 中,生成器是很普遍的,只是使用的时候没有觉察,例如 range(10) 返回的是一个类似生成器的对象;如果想要获得完整的列表,需要写成 list(range(10));

可迭代对象和迭代器对比

区别:从可迭代对象中,获取迭代器;迭代器如果迭代完毕,则不再可用,需要重新构建;

所谓的迭代器,可以理解为一个对象,每次调用它的 next 方法,可返回一个元素;如果空了,会报错;

通常迭代器还有一个 iter 方法,调用这个方法,可返回迭代器本身;理论上不实现它,也不会影响迭代功能。但如果实现了它,issubclass 方法可将其判断为 Iterator 的子类;

可迭代对象也有一个 iter 方法,调用它,会返回一个新的迭代器;

生成器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

# iter 使用了 yield 关键字,调用 iter 会返回生成器对象,此时 iter 是个生成器函数
def __iter__(self):
for word in self.words:
yield word

生成器函数的工作原理

yield 关键字有点像 await,每次执行到 yield 所在的语句时,会暂停等待;

for 循环语句会自动捕获并处理迭代器抛出的异常;

惰性实现

re 模块除了 findall 函数,还有一个生成器版本的 finditer 函数;它每次只返回一个匹配项;当数据量很大时,可以节省很多内存;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
def __init__(self, text):
self.text = text

def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)

def __iter__(self):
for match in RE_WORD.finditer(self.text): # finditer 返回生成器
yield match.group()

生成器表达式

生成器表达式有点像是列表推导的惰性版本;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>> def gen_AB(): # ➊
... print('start')
... yield 'A'
... print('continue')
... yield 'B'
... print('end.')
...
>>> res1 = [x*3 for x in gen_AB()] # res1 是一个列表,由生成器 gen_Ab 的返回值组成
start
continue
end.

>>> for i in res1: # ➌
... print('-->', i)
...
--> AAA
--> BBB

>>> res2 = (x*3 for x in gen_AB()) # res2 是一个生成器
>>> res2
<generator object <genexpr> at 0x10063c240>

>>> for i in res2: # 遍历 res2,此时 gen_AB 函数才真正的执行
... print('-->', i)
...
start
--> AAA
continue
--> BBB
end.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
def __init__(self, text):
self.text = text

def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)

# 使用表达式构建一个生成器,而不是用 yield 来生成
# 生成器表达式是一个语法糖,本质上跟使用 yield 的生成器函数没有区别
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))

何时用生成器表达式

生成器表达式是构建生成器的简捷方式,无需通过 def 定义函数来实现;但限于一些简单场景,一行可以搞定的那种;如果业务逻辑比较复杂,一行代码搞不定的话,则仍然需要使用函数来定义;

标准库 itertools 模块中有很多现成的生成器;

itertools.count(start, step):创建一个数字生成器

itertools.takewhile 给生成器添加条件限制

1
2
3
>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
>>> list(gen)
[1, 1.5, 2.0, 2.5]

标准库中的生成器

在创建任何生成器前,很有必要先查一下标准库中有哪些生成器可用,以避免重复造轮子;

yield from

yield from 可用来作为可迭代对象的生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

# 上面的写法,用 yield from 重写如下,可减少一层 for 循环

>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

可迭代的归约函数

归约函数:接受一个可迭代的对象,返回一个值,例如 reduce 函数;

深入分析 iter 函数

iter 函数用于生成迭代器,一般接收一个可迭代对象作为参数;但是它还有个用法是接收两个参数,第一个参数是个可迭代对象,第二个参数是个 predicate,当可迭代对象产生的值满足 predicate 时,就停止产出;

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def d6():
... return randint(1, 6)
...
>>> d6_iter = iter(d6, 1)
>>> d6_iter
<callable_iterator object at 0x00000000029BE6A0>
>>> for roll in d6_iter:
... print(roll)
...
4
3
6
3

生成器很适合用来处理大数据集,这样可以利用有限的内存,处理无限大的数据,例如大型数据库;

生成器当作协程

生成器对象有个 send 方法,该方法允许给生成器对象发送消息;

第15章 上下文管理器和 else 块

else

else 不仅可以跟 if 搭配使用,还可以跟 for, while, try 搭配使用;在这些场景中,else 实际上是 then 的意思,表示某个动作如果顺利完成了,那么就执行 else 里面的语句;如果没有顺利完成,就不执行;

1
2
3
4
5
for item in my_list:
if item.flavor == 'banana':
break
else: # 如果 for 循环结束,没有触发 break,那么就执行 else;如果触发,就退出循环,不执行else
raise ValueError('No banana flavor found!')
1
2
3
4
5
6
try:
dangerous_call()
except OSError:
log('OSError...')
else: # 如果 dangerous_call 顺利执行,没有报异常,则执行 else;如果报异常,就不执行 else
after_call()

with

with 的目标是安全的实现 try…finally;with 之后的表达式(例如 open 函数)会创建一个上下文管理器对象。该对象有两个方法,分别是 enter 和 exit;with 语句开始执行时,会调用 enter 方法;执行结束后,会调用 exit 方法,类似 finally 的作用;

1
2
3
4
5
6
# with...as... 是一个表达式,该表达式的前半段子句,会创建上下文管理器对象,并执行 enter 方法
# enter 方法执行完成后,会将结果返回到 fp 变量上,但 as 并不是必须的,有些场景并不需要返回什么东西
>>> with open('mirror.py') as fp:
... src = fp.read(60) #

# 当解释器执行完整个 with 块的语句后,会调用 exit 方法,清理现场
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 以下是一个上下文管理器类的示例
class LookingGlass:
def __enter__(self): # 做准备工作
import sys
self.original_write = sys.stdout.write
sys.stdout.write = self.reverse_write
return 'JABBERWOCKY'

def reverse_write(self, text): # 实际干活的方法
self.original_write(text[::-1])

def __exit__(self, exc_type, exc_value, traceback): # 做清理工作
import sys
sys.stdout.write = self.original_write
if exc_type is ZeroDivisionError:
print('Please DO NOT divide by zero!')
return True

contextlib 模块中有一些现成的工作,可用来创建自定义的 context 类(上下文管理器);

@contextmanager

contextmanager 装饰器可以简化上下文管理器的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import contextlib

@contextlib.contextmanager # 该装饰器会将 looking_glass 函数包装成带有 enter 和 exit 方法的类
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 'JABBERWOCKY' # 这里 yield 起到了类似分隔的作用,enter 执行到这里,后面由 exit 执行
except ZeroDivisionError:
msg = "Please DO NOT divide by zero"
finally:
sys.stdout.write = original_write
if msg:
print(msg)

个人感觉所谓的上下文管理器,本质上也像是一个实现了约定协议的鸭子类型,只要按照协议实现 enter 和 exit 方法即可;

第16章 协程

yield 单词本身有两个意思,一个是生成,一个是退让;这两个意思刚好是协程的描述;

当 yield 放在表达式的左边时,它做为生成器使用;

当 yield 放在表达式的右边时,它做为协程使用,等待传入值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def simple_coroutine(): # ➊
... print('-> coroutine started')
... x = yield # yield 右边没有值,意味着它生成 None
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro # 生成器已经创建,但是还没有启动,需要通过 next 让它启动
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro) # 通过 next 来启动生成器
-> coroutine started
>>> my_coro.send(42) # 给 yield 传值,仅当协程处于暂停状态时,才能够给它传值
-> coroutine received: 42
Traceback (most recent call last): # ➏
...
StopIteration

协和在 yield 关键字所在的位置暂停执行

使用协程重新设计平均值计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def averager():
total = 0.0
count = 0
average = None
while True: # 永远不会停止,可以无限计算平均值
term = yield average # 等待外部传入值,外部每传一次,就计算一次总体平均值
total += term
count += 1
average = total/count

# 以下是使用示例
>>> coro_avg = averager() # 创建
>>> next(coro_avg) # 激活
>>> coro_avg.send(10) # 传值
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

使用协程时,经常容易忘记要先激活它。为了避免这种错误,可考虑定义一个帮忙激活的装饰器

1
2
3
4
5
6
7
8
9
10
from functools import wraps

def coroutine(func):
"""定义一个装饰器:帮忙预激`func`"""
@wraps(func)
def primer(*args,**kwargs): ➊
gen = func(*args,**kwargs) ➋
next(gen) # 激活
return gen ➍
return primer

在调用生成器的 send 函数时,如果给它传递的参数类型有误,会导致它抛出异常,从而终止协程;

生成器有一个 throw 方法可用于触发异常;如果生成器内部有处理异常的代码,则执行;如果没有,则冒泡;

生成器还有一个 close 方法可用于抛出 exit 异常

yield from

yield from 带来了双向通讯机制,貌似可用来实现异步编程;先定义生成器,然后激活它;之后向它发送数据;当数据处理完成后,会触发异常,获得处理结果;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from collections import namedtuple

Result = namedtuple('Result', 'count average')

# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield # 感觉此处有点像是一个点位符,等待外部传值进来,或许应该叫 await
if term is None: # 当外部传 None 进来时,就中断退出循环
break
total += term
count += 1
average = total/count
return Result(count, average) # 中断循环后,返回计算结果

# 委托生成器
def grouper(results, key):
# 这里为什么要循环?
# 答:为了不断接收外面传进来的值
while True:
# 关键字 yield from 默认会让当前函数返回一个生成器,可惜这个关键字很不直观
# send 传进来的值,会通过传入 averager,yield from 有点像管道的作用
results[key] = yield from averager() # 这里的 yield from 很像 await

# 客户端(调用方)
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key) # grouper 返回生成器
next(group) # 预激活 # 激活后,开始进入 while 循环,在 yield from 处暂停
for value in values:
group.send(value) # send 将值传给 averager,开始与内部的子生成器通讯
group.send(None) # 传入None,中断子生成器,让委托生成器获得结果
print(results)

yield from 跟 await 有一个很大的不同,即 yield from 在将工作做到一半后,将控制权还给调用者,由调用者做剩下的工作;

据说 python 后来引入了 await

第19章 并发模型

协程: coroutine,一个可以暂停并重新运行自己的函数;

协程的特点在于可以通过关键字,标识出异步的位置,然后交出控制权,让主程序的其他部分获得控制权;自己则进入队列,等待异步的代码执行完毕;改变自己状态,等待被唤醒,继续运行自己余下部分的代码;

没想到 Python 有一个全局锁(GIL),每次只允许一个线程占有,那这就意味着 python 无法同时利用多核的优势好像,除非起多个进程,就像 js 的 cluster 一样;

Python 解释器每隔一定的时间(貌似是 5ms),会释放 GIL,以便其他线程能够获取锁;另外,任意一个函数在调用 syscall 时,它都会释放 GIL;

书里面的 spinner 案例,看起来很奇怪,因为协程的结束,竟然是由调用者的代码发起的,跟 js 好像不太一样;但是 await 貌似是一样的;

后来发现,协程也可以自己结束,不需要外部让它结束;书上的案例只是示范说可以主动干预。但其实正常使用场景是不干预,让它自行运行结束,返回结果;

问:好奇有无可能用装饰器,将非协程的代码,包装成协程代码?答:想了一下,虽然可能,但是由于非协程代码里面,在遇到 I/O 任务时,没有使用 await 交出控制权,该协程貌似可能会卡在那里等待;

协程能够起作用,貌似重点在于每次遇到 I/O 任务时,要主动交出控制权;在 js 里,很多库都是默认异步编写的,因此不容易忘记这个事情。但是在 python 里面,很多库并非天生异步,例如常用的 requests,此时很有必要提高警惕;

当协程获得控制权,处于运行中的状态时,它是无法被取消的。因为只有一个线程,当它在运行时,意味着想要取消它的代码并没有在运行;仅当协程位于队列中,处于等待状态时,才有可能被取消;此时取消它的代码有可能获得了控制权;

asyncio.run() 函数,做为所有协程运行代码的入口;

asyncio.create_task(),在当前协程中,创建一个新的协程;可基于返回的 task 对象,对新建的协程进行控制;

await coro(),调用 coro,并同步等待它返回结果;

调用 coro() 时,并不意味着 coro 的代码会马上执行,而只是表示将它加入了队列,实际的执行时间取决于事件循环的高度器;

跟 js 一样,await 关键字必须用在 async 定义的函数中;当函数用 async 定义时,它是一个协程;每次对该函数的调用,都是都它加入事件循环的队列中;而 await 表示交给当前协程对 CPU 的使用,即停止运行,让调度器去运行其他协程;等 await 的事件结束时,调度器会重新安排它运行;

GIL 的真实影响

各种处理网络请示的库,如 requests,在发起请示后,会释放当前线程的 GIL,以便其他线程可以抢占;但如果只有一个线程,那么抢占并没有意义。仅在多线程时,抢占才有意义;而且即使抢占成功,后续 requests 仍然会再次被分配 GIL,但此时它有可能仍然还没有取得响应,因此会浪费掉一些性能;但总的来说,多线程有助于提高 I/O 的并发处理能力;但不适用于 CPU 密集型的任务,性能反而变差,因为 CPU 不断在多个线程之间切换,但每次只运行一个线程,最终的性能还不如顺序执行来得快;

另外还有一些库的设计是异步的,但如果当前的代码不是 async 的话,貌似也无法使用 await 来交出控制权?

当处理计算密集型任务,因为 GIL 的存在,多线程是没有意义的,因为每次只有一个线程在工作;反而不如使用单线程来得简单和高效;如果有多核,则可以考虑使用多进程模式来提高效率;

第20章 Concurrent Executors

concurrent.futures 库里面,有两个类,分别是 ThreadPoolExecutor 和 ProcessPoolExecutor,它们可以很方便的使用线程或者进程来实现并发;

对使用者来说,背后的线程或进程是透明的,它会自动开启多线程或进程,同时创建任务队列,收集各线程的处理结果;

Python 里面的 futures 有点像 js 里面的 promise;但 futures 一般不直接创建,而是交由框架来创建;开发者可以在更高的抽象维度来使用它,这样可以避免错误使用;

future.done() 方法可用来查询是否计算完成了,但更常见的做法是不查询,而是等待通知,即完成后,调用回调函数即可;

future 有个 add_done_callback 方法,它接受一个回调函数做为参数;注意:该回调函数,将在运行该 future 的线程或进程中直接运行;

future.result() 方法可用于获取计算结果;但 concurrency 和 asyncio 两个库对方法的实现有所不同;concurrency 调用 result 方法时,会造成堵塞,等待结果的返回;同时支持 timeout 参数,超时未返回时,会抛出异常;asyncio 则不支持 timeout 方法,但支持 await 关键字,这样不会造成堵塞;

concurrency 还有一个 as_completed 方法,专门用来读取 result,以避免堵塞,

executor.map() 主要用于一个函数,并发处理多个不同的参数

executor.submit() 则更灵活一些,多个不同的函数,并处理各自不同的参数;最后通过 as_completed 方法收集计算结果;

第21章 异步编程

虽然可以通过 async def 来定义异步函数,但是如果函数体中包含一些非异步的操作,比如将文件写入本地,貌似该同步操作有可能会造成堵塞,占用整个线程,直到写入成功?经查证,发现确实如此,在异步函数中,只要有任意一个函数是非异步阻塞的,将导致整个调用链都是阻塞的;

其他

海象符 :=,为了省写一行代码,先检查,确认有值后,再赋值;没值的话,就不赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
# 没有海象符的时候
name = abc.get("name")
if name:
# doA
else:
# doB


# 有海象符后
if name := abc.get("name"):
# doA
else:
# doB

流畅的Python
https://ccw1078.github.io/2017/07/25/流畅的Python/
作者
ccw
发布于
2017年7月25日
许可协议