《流利的Python》第二版(Fluent Python edition 2)读书笔记

《流利的Python》第二版是基于python3.10版本的, 是一本对Python 3各个方面的特性的细节的介绍让你能够写出更精炼, 更快速, 更可读, 更pythonic的代码. 主要包括下面五个部分:

* 数据结构
* 函数对象
* 面向对象
* 控制流
* 元编程

之所以读这本书, 是因为python的进化很快, 之前一直在使用3.4编程, 可以通过这本书作为线索, 学习一下最新的python语言特性. 这读书笔记会专注于记录那些我不熟悉的新特性(或者老特性), 或者能够给我启发的内容.

没有翻译成中文的包括: 章节标题, 大部分专有名词, python的关键字相关的词.

Preface

How the Book Is Organized

Part I, 数据结构

第一章介绍的是Python数据模型, 解释为啥双下划线方法是所有类型对象一致行为的关键. 剩下的章节介绍容器类型的使用:包括sequences, mappings, sets, 和str vs bytes. 也介绍了标准库里的一些更高级的构造类: named tuple factories和@dataclass装饰器. Python3.10加入的模式匹配(Pattern matching)会在第2,3,5章介绍, 包括sequence patterns, mapping patterns, 和class patterns. Part I最后一章介绍对象的生命周期: references, mutability, garbage collection.

Part I 里面最感兴趣的是标准库里的容器类, 和python3.10的matching语法.

Part II, 函数对象

函数作为语言中的第一类对象: 什么意思, 它是如何影响一些流行的设计模式,以及如何通过利用闭包来实现函数装饰器. 这里还介绍了Python中可调用对象的一般概念, 函数属性, 自省(introspection), 参数注释和Python3中新的非本地声明. 第8章介绍了函数签名中的主要新主题--类型提示(pyte hints).

Part II 里面最感兴趣的是类型提示.

Part III 类与协议

这部分解释了如何构建容器类, 抽象基类, 协议, 多继承, 运算符重载. 第15章继续讨论类型提示.

Part IV 控制流

这部分介绍了超过条件,循环,子过程这种传统控制流的语言结构和库. 包括生成器, 上下文访问管理器(visit context managers)和协程, yield from语法. 第18章包括了使用pattern matching的例子. 第19章是新章节, 介绍了并行编程的一个可选项. 关于异步编程的章节也被重写了.

Part IV最感兴趣的是协程, with语句.

Part V 元编程(Metaprogramming)

这部分首先回顾了一个技术, 可以构建一种类, 该类具有能够动态构建的属性, 能处理半结构化数据(比如JSON数据集). 然后介绍了familiar属性机制, 然后深入探讨对象属性访问如何在 Python 中使用描述符在底层工作. 解释了函数, 方法和描述符之间的关系. 在第五部分中, 字段验证库的逐步实现揭示了导致最后一章的高级工具的细微问题: 类装饰器和元类.

全部感兴趣.

PART I 数据结构(Data Structures)

1. Chapter 1 The Python Data Model

这一章的内容就是介绍双下划线特殊方法, 没有什么新颖的. 这里就是学到一个新词dunder, 比如说__getitem__, 可以读为"dunder-getitem". dunder是"double underscore before and after"的缩写. 所以说下面三个是同义词special method, magic method, dunder method.

下面几个图总结得很有条理, 这里记录一下.

2. Chapter 2 An Array of Sequences

这一章主要讲各种序列(sequence)对象, 主题包括

  • list推导式(comprehension)和生成器表达式(generator expressions)
  • tuple用作记录(record) vs tuple用作不可变list
  • 序列解包(unpacking)和序列模式(pattern)
  • 切片对象的读和写
  • 特殊的序列类型, 比如array和queue

2.1. Overview of Built-in Sequences

两种分类方法:

  • 容器序列(list, tuple, collections.deque) vs 平铺序列(str, bytes, array.array)
  • 可变序列(list, bytearray, array.array) vs 不可变序列(tuple, str, bytes)

2.2. List Comprehensions and Generator Expressions

这两个概念对应的语法几乎是一样的, 唯一的区别就是前者用[]后者用(). 实际上, 前者生成一个list, 后这生成一个generator对象.

2.3. Tuples Are Not Just Immutable Lists

Tuples as Records也就是类似于c语言的struct, 只不过没有field名字, 只靠位置来访问. 可以方便的通过一个赋值语句来解包.

Tuples as Immutable Lists给出了几条用tuple替代list的优势, 主要就是长度已知和速度更快.

下面这张比较list, tuple两种对象方法和属性的表值得收藏.

2.4. Unpacking Sequences and Iterables

序列解包, 可以避免使用索引(index)运算. 解包操作可以被使用到所有iterable对象上, 不光sequence对象.

最基本的解包操作就是并行赋值(parallel assignment).

另一个解包操作就是在函数调用时, 给一个参数加前缀*. 例子如下:

1
2
3
4
5
6
7
8
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)

加*前缀拆包还有以下几个用途

2.4.1. Using * to Grab Excess Items

主要用于并行赋值(parallel assignment), 抓取额外的元素.

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

2.4.2. Unpacking with * in Function Calls and Sequence Literals

在函数调用时, 解包之后的可迭代对象可以被用作相应位置的输入参数.

1
2
3
4
5
>>> def fun(a, b, c, d, *rest):
... return a, b, c, d, rest
...
>>> fun(*[1, 2], 3, *range(4, 7))
(1, 2, 3, 4, (5, 6))

也可以用在定义list, tuple, set的文本(Literal)里, 比如:

1
2
3
4
5
6
>>> *range(4), 4
(0, 1, 2, 3, 4)
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}

2.4.3. Nested Unpacking

嵌套的tuple可以直接嵌套式解包, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for name, _, _, (lat, lon) in metro_areas:
if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

if __name__ == '__main__':
main()

2.5. Pattern Matching with Sequences

python 3.10加入的新语法match/case 语句 在这本书里根据pattern的类型, 被拆分到各个章节. 这节只介绍sequence的pattern.

看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
def handle_command(self, message):
match message:
case ['BEEPER', frequency, times]:
self.beep(times, frequency)
case ['NECK', angle]:
self.rotate_neck(angle)
case ['LED', ident, intensity]:
self.leds[ident].set_brightness(ident, intensity)
case ['LED', ident, red, green, blue]:
self.leds[ident].set_color(ident, red, green, blue)
case _:
raise InvalidCommand(message)

  1. 关键字match后面的表达式是主语(subject), Python将要尝试把主语匹配到每个case分句中的pattern上.
  2. 第一个pattern会跟任何包含三个元素的sequence进行匹配, 第一个元素必须是字符串'BEEPER'. 第二个和第三个可以是任何值, 他们会被按顺序绑定到变量frequencytimes.
  3. 第二个pattern匹配任何具有两个元素且第一个是字符串'NECK'的主语.
  4. 第三个pattern将匹配任何具有三个元素且第一个元素是字符串'LED'的主语. 如果元素个数不匹配, Python会继续尝试匹配下一个case.
  5. 第四个pattern将匹配首元素是'LED'的5元素主语.
  6. 最后一句是默认case. 任何在前面pattern中找不到匹配的主语将匹配这一条, _是个特殊变量, 后面会讲到.

这里比较了match/case跟C语言里的switch/case的区别, 作者认为:

  1. 原来python中的if/elif/elif/.../else语句块是switch/case的一个很好的替代品, 而且避免了c语言中常碰到的fallthrough和dangling else问题. 前者是忘记写break时产生的, 后者是if-else不写大括号导致嵌套if语句的是else匹配跟预想的不一致.
  2. match/case比switch/case强大, 一个重要的改进就是解构(destructuring), 也就是可以在匹配时直接对主语进行拆包(unpacking)

通常来讲, 当下列条件都满足, 一个sequence pattern匹配到主语(subject):

  1. 主语是一个sequence;
  2. 主语跟pattern有相同数量的元素;
  3. 每一个对应的元素匹配, 包括嵌套的元素.

例如pattern [name, _, _, (lat, lon)]匹配一个具有4个元素的sequence, 并且其最后一个元素是具有两个元素的sequence.

几点额外的用法和说明:

  1. sequence pattern可以是list, tuple, 或嵌套的tuples和lists的组合. 在pattern里方括号和园括号没有区别.
  2. 一个sequence pattern可以匹配大多数collection.abc.Sequence的子类的实例, 但str, bytes, bytearray除外. 这三个类型如果做match语句主语, 会像整数一样被当作单一值处理.
  3. 标准库里下面这些类型与sequence patterns兼容: list, tuple, memoryview, range, array.array, collections.deque.
  4. 与前面讲的拆包不同的地方是, 不能对非sequence得可迭代对象解构(destructuring).
  5. 符号_在这里是特殊的, 它将匹配在该位置的任何元素, 但是不绑定名字. _也是唯一可以多次出现在pattern里的元素.
  6. 可以用as关键字绑定pattern的一部分到一个变量, 比如case [name, _, _, (lat, lon) as coord]:
  7. 可以在pattern里添加类型信息, 例如case [str(name), _, _, (float(lat), float(lon))]:, 这里语法跟构造器调用的语法一样, 但是含义不一样.
  8. *_可以用来表示任意数量的元素, 例如case [str(name), *_, (float(lat), float(lon))]:可以匹配任何以字符串开头, 以两个浮点类型元素的嵌套sequence结尾的主语. 也可以用*extra来替换*_, 这么做extra会绑定到一个0到多个元素的list对象. 在一个sequence pattern里, *只能出现一次. 但嵌套的sequence可以分别使用, 例如case ['lambda', [*parms], *body] if body:
  9. 一个可选的以if开头的守卫从句可以用来添加匹配条件. 但if语句只有当匹配了pattern才会被执行. case之后的语句块只有匹配pattern且守卫表达式为真时才会被执行, 例如:
1
2
3
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

2.6. Slicing

这里作者讲了一个有趣的知识点, 执行seq[start:stop:step]在python内部调用了seq.__getitem__(slice(start,stop,step)]. 所以可以给一个slice对象命名, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> invoice = """
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY = slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)
>>> line_items = invoice.split('\n')[2:]
>>> for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION])
...
$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]

2.7. Using + and * with Sequences

几个知识点:

  1. 对sequence进行+*, 会创建新的对象, 而不会修改操作数.
  2. 构造多维数组时, 有个陷阱, 这个我过去也碰到过, 正确做法:
1
2
3
4
5
6
>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

错误做法:

1
2
3
4
5
6
>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

下面是关于对sequence类型应用+=, *=这种增量赋值(Augmented Assignment)的几个知识点

  1. 对于a += b, 如果实现了__iadd__, 那么这个dunder函数将被调用. 如果用于可变序列(mutable sequences), 那么该对象将被修改(类似于a.extend(b)). a讲指向原来的对象.
  2. 如果没有实现__iadd__, 但是实现了__add__, 那么a += ba = a + b效果相同. 所以a += b运行之后a指向的a + b产生的新对象. 对于不可变序列对象(immutable sequences), 这个是必然结果.
  3. +=在底层相当于两步操作, 所以有可能完成一半之后报错, 比如下面这个例子:
1
2
3
4
5
6
7
>>> t=(1,2,[30,69])
>>> t[2] += [50, 40]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 69, 50, 40])
  1. 避免在tuple里使用可变类型元素.
  2. 网站pythontutor.com可以帮助分析内存分配. 直接分析字节码也不是很难.

2.8. list.sort Versus the sorted Built-in

list.sort不创建新对象, 内置函数sorted创建新的对象. 两个参数reverse是逆序不用说, key是以可迭代对象的每个元素为唯一参数的一个函数, 这个函数返回排序时用来比较的对象. 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple']
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']
>>> fruits.sort()
>>> fruits
['apple', 'banana', 'grape', 'raspberry']

一个额外的知识点, 利用bisect内置库模块可以在维持排序的情况下做插入和快速搜索.

2.9. When a List is Not the Answer

list类型非常易用, 但针对一些特定的需求, 有时候有更好的选择. 这一小章节就是介绍其他选项的, 我也经常过度使用list, 所以这章认真读一下.

2.9.1. Arrays

如果list只包含数字, 那么array.array是很有效的替代品.

  1. Arrays支持所有可变序列的操作, 比如.pop, .insert, `.extend.
  2. 有课外的快速读写(saving, loading)的操作, 比如.frombytes, .tofile
  3. 跟C array一样节省.
  4. 无法像list一样自排序, 需要使用built-in sorted函数来做排序. 例如a = array.array(a.typecode, sorted(a)). 使用bisect.insort可以在保持排序的情况下添加元素.

下表是跟list的方法对比. 这本书这种对比特别直观, 点赞收藏.

2.9.2. Memory Views

memoryview居然是个built-in的类, 可以让不同的sequence类型共享内存. memoryview.cast方法可以通过指定内存空间内每个元素的大小, 和对元素进行重新分组来修改读写元素的方式. 这个方法返回另一个memoryview对象, 但跟源对象共享同一块内存.

2.9.3. NumPy

NumPy提供高级的数组和矩阵运算, 它是Python能够成为主流的科学计算程序语言的原因. SciPy是基于NumPy的一个库, 提供大量科学计算算法, 包括线性代数, 微积分, 统计等.

NumPy和SciPyye也是一些其他工具的基础, 比如

  1. Pandas提供了有效的非数字类型的数组操作, 提供了导入/导出到其它格式, 像.csv, .xls, SQL, HDF5等等.
  2. scikit-learn是当前最广泛使用的机器学习工具集.
  3. Dask项目, 提供了在一组机器上并行计算Numpy, Pandas, scikit-learn过程的能力.

2.9.4. Deques and Other Queues

collections.deque类是一个线程安全(thread-safe)的双向序列. 可以从两端快速插入和删除. deque可以被固定最大长度, 叫做绑定的deque(bounded deque). 如果绑定的deque满了, 那么从一端加入新元素, 将导致另一端的第一个元素被删除.

下表是list和deque方法对比:

除了deque, 其他的python标准库里如下一些模块还包括了其他的一些队列类:

  1. queue: 这个模块提供了thread-safe的同步类, 包括SimpleQueue, Queue, LifoQueue, PriorityQueue. 除了SimpleQueue, 其他几个类可以在构建时提供一个最大长度. 但跟deque不同的是, 队列满了之后不会删除元素, 而是会阻塞线程, 直到有其他线程给队列腾出空间. 这对于线程间同步非常有用.
  2. multiprocessing: 这个模块实现了它自己的SimpleQueue和绑定的Queue, 跟模块queue里的很像, 但是是为了进程间交互设计的. 为了任务管理提供了一个特别的multiprocessing.JoinableQueue类.
  3. asyncio: 提供了Queue, LifoQueue, PriorityQueue, 和JoinableQueue. 跟模块queue里的很像, 但是为了异步编程的任务管理设计.
  4. heapq: 跟前三个模块不同, heapq没有实现队列类, 但是提供了一些像heappush, heappop的函数, 让用户可以像使用heap queue或者priority queue一样使用一个可变队列(mutable sequence).

3. Chapter 3 Dictionaries and Sets

Python的一些核心基础结构是由内存中的字典结构构成的, 比如:

  1. 类和实例的属性.
  2. 模块名字空间.
  3. 函数的keyword参数.
  4. __builtins__.__dict__存储所有的内置类型, 对象, 和函数.

哈希表(Hash tables)是Python高性能dict的基础引擎. set, frozenset也是基于哈希表.

3.1. Modern dict Syntax

3.1.1. dict Comprehensions

例子:

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

3.1.2. Unpacking Mappings

从Python3.5版本开始映射类型的解包(mapping unpacking)可以用有两种方式的应用:

  1. 在函数调用的时候, 可以应用**到多个参数上.
  2. **可以被用于dict的文本里.

例子:

1
2
3
4
5
6
7
>>> def dump(**kwargs):
... return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}

3.1.3. Merging Mappings with |

Python 3.9版本支持使用||=来合并mapping类型. 前者创建一个新的mapping对象, 新对象的类型默认跟左边的操作数相同, 后者更新现存的mapping.

3.2. Pattern Matching with Mappings

match/case语句的主语(subject)可以是mapping对象. Pattern看起来跟mapping文本很像, 可以匹配任何collections.abc.Mapping的子类的实例.

pattern matching是用来处理嵌套的mappings和sequences的结构化记录非常强大的工具. 比如处理JSON, 半结构化数据库比如MongoDB, EdgeDB, PostgreSQL.

下面是个例子

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
>>> def get_creators(record: dict) -> list:
... match record:
... case {'type': 'book', 'api': 2, 'authors': [*names]}: # <1>
... return names
... case {'type': 'book', 'api': 1, 'author': name}: # <2>
... return [name]
... case {'type': 'book'}: # <3>
... raise ValueError(f"Invalid 'book' record: {record!r}")
... case {'type': 'movie', 'director': name}: # <4>
... return [name]
... case _: # <5>
... raise ValueError(f'Invalid record: {record!r}')
...
>>> b1 = dict(api=1, author='Douglas Hofstadter',
... type='book', title='Gödel, Escher, Bach')
>>> get_creators(b1)
['Douglas Hofstadter']
>>> from collections import OrderedDict
>>> b2 = OrderedDict(api=2, type='book',
... title='Python in a Nutshell',
... authors='Martelli Ravenscroft Holden'.split())
>>> get_creators(b2)
['Martelli', 'Ravenscroft', 'Holden']
>>> get_creators({'type': 'book', 'pages': 770})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in get_creators
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
>>> get_creators('Spam, spam, spam')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in get_creators
ValueError: Invalid record: 'Spam, spam, spam'

这里需要注意的几点是

  1. pattern里面key的顺序不会影响匹配, 即便主语是OrderedDict.
  2. 跟sequence pattern对比, mapping pattern允许部分匹配.
  3. 因为允许部分匹配, 所以不需要用**extra匹配额外的key-value对. 但如果你想要捕获额外的key-value对, 作为一个dict, 最后一个变量可以用双星号变量, 而且不能是**_.

3.3. Standard API of Mapping Types

dict的接口是由collections.abc模块提供的MappingMutableMapping描述的, 见下图:

想要自定义mapping类型, 可以扩展collections.UserDict, 或者通过composition包裹一个dict, 而不是这些ABCs的子类. collections.UserDict类和标准库中的所有具体mapping类在其实现中都封装了基本dict. 而dict又构建在哈希表上. 因此. 它们都有一个限制. 即键必须是可散列(hashable)的(value不需要是可散列的,只需要key).

3.3.1. What is Hashable

定义: 如果一个对象具有一个哈希值, 并且这个值在对象的整个生命周期内不变(这需要一个__hash__()方法). 并且可以跟其他对象进行比较(这需要一个__eq__()方法), 那么这个对象就是可散列的(hashable). 两个相等的可散列对象必须具有相同的哈希值.

  • 可散列对象包括: 数值类型, flat immutable类型(str, bytes), 所有元素都是可散列的的immutable容器(frozenset, tuple if all its items are hashable).
  • 不可散列对象包括: mutable容器类(list, dict)

其他几个知识点:

  1. 同一个对象的哈希值只有同一个python进程里能确保相等. 在不同的机器, 不同python版本, 不同python的实现, 不同的python进程里都不能确保相同. 其中一个原因是为了安全性的原因(PEP456), 在每个进程计算hash的时候加入一个随机因子(英语里叫撒点盐salt).
  2. 用户定义类型默认是hashable, 因为默认他们的哈希值就是他们的id(), 而__eq__()方法继承了根类object, 简单比较对象ID. 如果对象实现了__eq__()方法, 那么它只有实现了__hash__()方法, 并且其每次能返回相同值, 它才是hashable. 所以实践中, 这两个dunder方法仅使用那些在对象生命周期内保持不变的属性.

3.3.2. Overview of Common Mapping Methods

下面这个表给出了dict和它的两个流行的变体defaultdictOrderedDict. 这两个变体定义在collections模块.

3.3.3. Inserting or Updating Mutable Values

dict访问操作d[k]里的k不是一个存在的键, 会报错. d.get(k, default)比处理KeyError更方便的返回默认值. 如果想要同时取值和更新, 可以使用d.setdefault(key,default), 这个方法虽然叫set somthing, 但是意思是, 如果key存在就返回d[key], 否则先d[key]=default, 然后再返回d[key]. 这样可以减少一次搜索.

3.4. Automatic Handling of Missing Keys

在一个mapping对象搜索一个不存在的key, 对象的行为是可以定制化的. 通常通过两种可能的途径, 一个是用defaultdict代替默认的dict, 另一个是继承dict, 添加__missing__方法来定制行为模式.

3.4.1. defaultdict: Another Take on Missing Keys

知识点:

  1. collections.defaultdict实例初始化的时候, 你需要提供一个可调用对象.
  2. 当一个不存在的key被传给__getitem__方法时, 这个可调用对象被调用来产生一个默认值.
  3. 这个可调用对象也作为实例的一个叫做default_factory的属性.
  4. d.get(k)将不会触发default_factory被调用.

3.4.2. The __missing__ Method

想要更精确的定制处理不存在key的行为, 需要使用__missing__方法, dict类没有定义这个方法, 但是如果任何子类提供了这个方法, 当不存在key被访问的时候, dict.__getitem__会调用这个方法.

知识点:

  1. 避免出现无限迭代的情况.
  2. 为了行为一致性, __contains__方法也需要在子类重写. 因为表达式k in d会调用这个方法, 但继承自dict__contains__并不会回调__missing__.

例子:

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

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

3.4.3. Inconsistent Usage of __missing__ in the Standard Library

几个场景

dict的子类 只实现了__missing__ 那么只有d[k]通过继承自dict__getitem__ 有可能调用__missing__.
collection.UserDict的子类 只实现了__missing__ 由于继承自UserDictget方法也调用__getitem__ 所以d[k]d.get(k)都有可能调用__missing__
abc.Mapping的子类 实现最简单的__getitem__ __missing__永远不会被调用, 因为__getitem__ 没调用它.
abc.Mapping的子类 __getitem__调用__missing__ d[k], d.get(k), k in d都会触发__missing__

这四个场景只描绘了最小化的实现, 如果你的子类实现了__getitem__, get, __contains__, 那么你可以基于你的需求决定这些方法是否使用__missing_. 关键点是标准库中的mappings的默认行为是不一样的, 所以一定要小心使用.

别忘了setdefaultupdate也受搜索key的影响, 所以你有可能需要实现具有特殊逻辑的__getitem__来避免对象行为的不一致性.

3.5. Variations of dict

这一节介绍标准库里除了defaultdict以外的mapping类型.

3.5.1. collections.OrderedDict

Python 3.6开始, 内建的dict也会保留keys的顺序. Python文档里列出的dictOrderedDict的区别如下.

  1. OrderedDict在判定等号时会检查顺序是否相同.
  2. OrderedDictpopitem()方法会接受一个可选参数来指定时LIFO还是FIFO.
  3. OrderedDict有一个move_to_end()方法可以有效的把元素挪到队尾.
  4. 运算效率上, dict优先mapping操作, 而追踪插入元素的顺序次之.
  5. 运算效率上, OrderedDict优先重排序操作, 空间效率, 迭代速度和更新操作次之.
  6. 算法上, OrderedDictdict更善于处理频繁排序的操作. 因此很适合用于追踪最近访问的操作(比如LRU缓存).

3.5.2. collection.ChainMap

  1. ChainMap实例可以把多个mapping类型对象当作一个列表来检索. 检索操作是按照构建时提供的mapping类型对象的顺序来执行的, 只要找到任何一个存在的key, 就检索成功.
  2. 更新和插入操作只影响第一个mapping对象
  3. 不拷贝对象, 只保存mapping对象的引用.
  4. 特别适合用于语言的嵌套名字哦那空间.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6
>>> chain['c'] = -1
>>> d1
{'a': 1, 'b': 3, 'c': -1}
>>> d2
{'a': 2, 'b': 4, 'c': 6}
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))

3.5.3. collection.Counter

  1. 这个对象可以保存每个key的整数计数.
  2. 更新对象实际上是在现存计数上加新计数.
  3. +-运算用于合并计数.
  4. most_common([n])返回一个有序的list, 元素是tuple, 表示计数最多的前n个元素.

例子:

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')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(3)
[('a', 10), ('z', 3), ('b', 2)]

3.5.4. shelve.Shelf

标准库中的shelve模块为从字符串key到用pickle二进制格式序列化的Python对象的mapping提供了一个永久存储的方法. 名字叫shelve, 来自于咸菜(pickle)坛子放在架子(shelve)上.

shelve.open函数返回一个shelve.Shelf实例, 这是一个简单的key-value DBM数据库.

3.5.5. Subclassing UserDict Instead of dict

这段建议大家如果想创建自己的mapping类型, 继承UserDict而不是dict. 主要原因是dict的实现里有一些shortcuts. 这导致一些本来可以直接继承UserDict的方法, 在继承dict时需要重写.

3.6. Immutable Mappings

types.MappingProxyType 可以作为mapping类型的一个只读代理来使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>

3.7. Dictionary Views

dict实例的几个方法.keys(), .values(), .items()返回下面几个类的实例: dict_keys, dict_values, dict_items. 这几个是dict实际数据结构的一个只读的投影, 避免了像python2一样的内存占用. 这几个类是dict专用的, 不能用[]进行元素访问, 也不能使用类直接实例化. 只实现了__len__, __iter__, __reversed__几个特殊方法.

3.8. Practical Consequences of How dict Works

  1. Keys必须是hashable的对象.
  2. 通过key来访问dict的元素非常的快. 一个dict可能含有上百万个keys, 但python可以直接通过key的哈希值直接定位.
  3. python3.6里key的顺序是更紧凑的内存布局的副作用, 在3.7中称为一个正式的特性.
  4. 尽快采用了新的紧缩内存, 依然有额外的内存开销. 而不是像Tuple采用的最紧缩布局 -- 容器是指向对象的引用的数组. 哈希表需要额外的空行来保持效率.
  5. 避免在__init__方法以外添加实例属性, 可以节省内存. 原因是python3.3开始, 内部设计是一个类的所有实例可以共享一个公共的哈希表, 通过每个新实例的__dict__. 但如果在__init__之后动态添加一个属性会强迫python为该实例创建一个新的哈希表. 见PEP412.

3.9. Set Theory

setfrozenset最早作为module出现在python2.3, 后来在python2.6加入built-in类型. 虽然出现得早, 但还是没被充分利用.

  1. 一个set是unique对象的集合. 一个基本的用法是去除重复对象. 但不能保证顺序. 若想去掉重复还保证顺序, 有一个技巧, 如下.
    1
    2
    3
    4
    >>> dict.fromkeys(l).keys()
    dict_keys(['spam', 'eggs', 'bacon'])
    >>> list(dict.fromkeys(l).keys())
    ['spam', 'eggs', 'bacon']
  2. set的元素必须是hashable的, 但set本身不是hashable的. frozenset是hashable的, 可以嵌套进其它set.
  3. set的几个中缀集合运算, 包括 a | b返回合集, a & b返回交集, a - b返回差集, a ^ b返回对称差集(symmetric difference, 即(a-b)|(b-a)). 合理使用将使代码即快又有可读性.

3.9.1 Set Literals

  1. set 常量表达式看起来跟数学上的记号没啥区别, 除了一点, 就是没有空集. 表达式{}会返回一个空dict. 正确的空集用set()表示.
  2. 对于set类型, 常量表达式{1,2,3}比调用构造函数set([1,2,3])运行得更快. 因为前者直接使用BUILD_SET字节码, 后者还要创建list对象, 搜索构造函数等工作.
  3. frozenset只能调用构造函数, 比如
    1
    2
    >>> frozenset(range(10))
    frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

3.9.2 Set Comprehensions

  1. dict类似, 就是括号中间没有:.
    1
    2
    3
    4
    >>> from unicodedata import name
    >>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')}
    {'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©',
    '°', '+', '÷', '±', '>', '¬', '®', '%'}
  2. 顺序每个process不同, 是因为哈希计算撒盐的原因.

3.10 Practical Consequences of How Sets Work

  1. 元素必须是hashable对象. 实现__hash____eq__方法.
  2. 验证一个对象是否是元素非常高效, 是通过计算哈希值来确定的.
  3. 集合对象使用的内存要比数组对象多.
  4. 元素的顺序跟依赖于插入的顺序, 但是不完全可信.
  5. 加入新元素到一个集合对象, 有可能改变现存元素的顺序. 原因是一旦哈希表已经占用超过三分之二, python内部算法可能需要移动和扩展这个哈希表, 这个过程中现有的元素需要被重新插入, 他们的相关顺序有可能会变.

3.10.1 Set Operations

注意:

  1. 下表中的一些运算符或者方法会修改集合对象本身(例如, &=, difference_update等), 这些方法不会在frozenset中实现.
  2. 双元运算符(infix operators)需要两边的操作数都是集合类型. 但是所有其他方法可以以可迭代对象为参数. 例如a.union(b, c, d)中a必须是set类型, 但是b, c, d可以是可迭代对象.
  3. 如果不是更新现存的对象, 而是从4个可迭代对象创建一个新集合对象, 从python3.5以后可以用{*a, *b, *c, *d}这样的表达式.

3.11 Set Operations on dict Views

  1. 前面章节说过dict方法.keys(), .items()会返回视图对象, 这几个对象跟frozenset非常相似. 下面这个表展示了他们的实例方法.
  2. 这几个试图对象还支持一些集合运算符, 合集(&), 交集(|), 差集(-), 对称差集(^).
  3. dict_ites只有当所有values都是hashable的, 才能像set一样操作.

4. Unicode Text Versus Bytes

Python3把人类使用的符号的字符串和字节序列明确的区分. 不能像python2一样隐藏转换. 这一章讨论下面几个话题:

* 字符, code points和byte的表示
* 二进制序列:`bytes`, `bytearray`和`memoryview`的特性.
* Unicode的编码和过去的字符集的编码
* 避免和处理编码错误
* 处理text文件的最佳实践
* 默认编码的陷阱和标准I/O的问题
* 规范化的安全的Unicode文本的比较
* 几个用于nomalization, case folding和brute-force的实用函数
* 通过`locale`和`pyuca`库对Unicode文本进行排序
* Unicode数据库的字符元数据
* 处理`str`和`bytes`的双模式API

字符串和字节串这方面过去搞清楚过, 但是时间长了又忘了. 这里正好捋清楚.

4.1. Character Issues

几个概念:

  1. 字节(byte), 一个8比特的整数.
  2. 字符(character), Unicode字符, 就是人类使用的各种符号.
  3. 字符串(string), 字符的序列
  4. 码点(code point), 码点是字符的唯一编号, 范围是从0到1114111. Unicode标准里用前缀U+加上4到6位十六进制数字表示.
  5. 编码(encoding)和解码(decoding), 编码就是一种把码点转换成一个字节序列的算法. 反过来就是解码.

4.2. Byte Essentials

基础知识:

  1. python3里又两个内置类bytes, bytesarray来表示字节序列, 后者是immutable的.
  2. 这两个类实例的每个元素是一个0到255的整数.
  3. 对序列进行切片操作返回一个跟原序列相同类型的字节序列.
  4. 字节序列的元素访问操作my_bytes[0]返回一个整数, 字符串的元素访问操作返回一个字符串. 这里字符串比较特殊.

显示一个字节序列的时候, ASCII通常嵌入到显示中, 这里有4种情况:

  1. 十进制32-126, 会显示ASCII字符本身.
  2. 制表符, 换行符, 回车符, \会用转义符显示, \t, \n, \r, \\.
  3. 如果单双引号都在序列中, 那么显示的时候用单引号当作分隔符, 序列中的单引号用转义符\'表示.
  4. 其他所有的字节用一个转义符加x加两位十六进制数字表示, 比如\x00表示null字节.

初始化一个bytes可以有以下几个方法:

  1. bytes的类方法fromhex.
  2. 用字符串和编码作为参数的构造函数.
  3. 用元素是0-255的值的可迭代对象作为参数的构造函数.
  4. 用实现了buffer协议的对象(bytes, bytearray, memoryview, array.array)作为参数的构造函数. 这样会拷贝源对象的字节序列到新对象.
  5. Python3.5以前可以用一个整数当作参数调用构造函数, 可以创建一个长度是该整数, 全部元素都是null的字节序列. 这个特性在Python3.6时被移除了(PEP-467).

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> cafe = bytes('café', encoding='utf_8')
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0]
99
>>> cafe[:1]
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:]
bytearray(b'\xa9')
>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> octets = bytes(numbers)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'

4.3. Basic Encoders/Decoders

Python支持100多种编码方式(codecs=encoder/decoders). 每个编码方式都有名字像utf_8, 和别名, 比如utf8, utf-8. python文档里standard-encodings可以找到这些名字的列表.

1
2
3
4
5
6
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
... print(codec, 'El Niño'.encode(codec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

4.4. Understanding Encode/Decode Problems

4.4.1. Coping with UnicodeEncodeError

除了UTF编码方式, 其他编码方式只能处理Unicode符号的一个子集. 当把文本转成字节的时候如果符号在指定的编码方式中没有定义, 那么就会抛出UnicodeEncodeError异常, 除非指定的处理方式被传递给编码函数. 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> city = 'São Paulo'
>>> city.encode('utf_8')
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1')
b'S\xe3o Paulo'
>>> city.encode('cp437')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore')
b'So Paulo'
>>> city.encode('cp437', errors='replace')
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace')
b'S&#227;o Paulo'

ASCII是所有编码方式的子集, 用str.isascii()来检查一个文本是否纯ASCII编码方式, 可以避免UnicodeEncodeError异常.

4.4.2. Coping with UnicodeDecodeError

如果解码的时候碰到对于该编码方式是无效的字节, 那么会抛出UnicodeEncodeError.有些过去的8比特编码方式(比如cp1252, iso8859_1)能够解码任何字符流, 所以如果使用这些编码方式, 程序将不会报错, 但是对于错误的字节不会报错, 而是转换成乱码.

1
2
3
4
5
6
7
8
9
10
11
12
>>> octets = b'Montr\xe9al'
>>> octets.decode('cp1252')
'Montréal'
>>> octets.decode('iso8859_7')
'Montrιal'
>>> octets.decode('koi8_r')
'MontrИal'
>>> octets.decode('utf_8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
>>> octets.decode('utf_8', errors='replace')

4.4.3. SyntaxError When Loading Modules with Unexpected Encoding

python打开一个文件时默认该文件是用UTF-8进行编码存储的, 如果不是可以在文件开头加如下一行来指定:

1
# coding: cp1252

4.4.4. How to Discover the Encoding of a Byte Sequence

如果只有一个字节序列, 怎么能知道用的什么编码方式? 简单答案是你不能知道, 只能是被告知. 但是可以根据某些编码生成的字节序列的特性进行猜测. Chardet是一个python库可以用来猜测30多种encodings.

4.4.5. BOM: A Useful Gremlin

知识点:

  1. UTF-16开头有可能有几个字节代表大小尾. 被叫做BOM(byte-order mark). 这个Unicode字符是U+FEFF(ZERO WIDTH NO-BREAK SPACE), 大尾时顺序是b'\xfe\xff', 小尾是b'\xff\xfe'.
  2. 一个变体是UTF-16LE和UTF-16BE. 如果使用这两个BOM就不需要.
  3. 如果一个UTF-16格式的文件没有BOM, 根据Unicode标准默认应该是大尾, 但由于Intel x86是小尾芯片, 所以有大量的没有BOM却是小尾存储的UTF-16的文件存在.
  4. 大小尾问题之影响多于一个字节代表字符的编码, 比如UTF-16, UTF-32. UTF-8最大的优势就是编码之后跟大小尾无关. 所以也不需要BOM. 然而一些windows的应用(著名的Notepad)会给UTF-8文件添加BOM. 这种编码被叫做UTF-8-SIG. 字符U+FEFF用UTF-8编码是b'\xef\xbb\xbf'. 所以如果文件开头是这三个字节. 很有可能是带BOM的UTF-8文件.

4.5. Handling Text Files

处理文本I/O的最佳实践是Unicode三明治模式, 意思是bytes应该尽快解码成str, 然后当作字符串进行处理, 处理完尽量晚的编码为字节序列.

4.5.1 Beware of Encoding Defaults

如果不指定编码格式, 那么在linux/macOS上, 默认都是UTF-8. 在windows上就复杂了:

  1. 如果打开文件的时候忽略了encoding参数, 默认是用locale.getpreferredencoding()的返回. (cp1252如果windows地区设置成US)
  2. sys.stdout|stdin|stderr使用的编码格式逻辑如下:
    1
    2
    3
    4
    5
    6
    7
    8
    if (环境变量PYTHONIOENCOD存在
    and ((Python版本 < 3.6)
    or ((Python版本 >= 3.6)
    and 环境变量PYTHONLEGACYWINDOWSSTDIO存在))) then:
    if 交互环境I/O:
    默认使用UTF-8
    elif I/O被重定向到文件:
    默认使用locale.getpreferredencoding()的返回值作为编码格式.
  3. Python内部对bytes和str互相转换的默认编码格式是sys.getdefaultencoding()的返回值.
  4. Python打开操作系统文件时, 传给OS API的文件名字的默认编码格式是sys.getfilesystemencoding()的返回值.

如此复杂, 所以最好的办法就是避免使用默认编码格式, 每次编码解码时都明确指定编码格式.

4.6. Normalizing Unicode for Reliable Comparisons

由于Unicode允许通过把变音符号(diacritics)添加到其他字符, 实际显示出一个组合字符. 这增加了字符串比较的复杂性.

例子:

1
2
3
4
5
6
7
8
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False

解决办法是使用规范化函数unicodedata.normalize(), 这个函数的第一个参数, 可以是'NFC', 'NFD', 'NFCK', 'NFKD'.

  1. Normalization Form Compose (NFC)会组合码点产生最短的等效字符串. Normalization Form Decompose (NFD)会把字符扩展成基础字符和附加字符. 键盘输入的默认是NFC类型.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    >>> from unicodedata import normalize
    >>> s1 = 'café'
    >>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
    >>> len(s1), len(s2)
    (4, 5)
    >>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
    (4, 4)
    >>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
    (5, 5)
    >>> normalize('NFC', s1) == normalize('NFC', s2)
    True
    >>> normalize('NFD', s1) == normalize('NFD', s2)
    True
    >>> ohm = '\u2126'
    >>> name(ohm)
    'OHM SIGN'
    >>> ohm_c = normalize('NFC', ohm)
    >>> name(ohm_c)
    'GREEK CAPITAL LETTER OMEGA'
    >>> ohm == ohm_c
    False
    >>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
    True

  2. NFKC和NFKD, K=compatibility=兼容字符, 兼容字符是只为了兼容其他标准存在的字符. 在这两种类型中, 一个兼容字符可以被拆解为一个或者多个被认为更好表示的其他字符, 虽然这种拆分有可能会丢失格式信息. 但方便搜索和索引操作.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    >>> from unicodedata import normalize, name
    >>> half = '\N{VULGAR FRACTION ONE HALF}'
    >>> print(half)
    ½
    >>> normalize('NFKC', half)
    '1⁄2'
    >>> for char in normalize('NFKC', half):
    ... print(char, name(char), sep='\t')
    ...
    1 DIGIT ONE
    ⁄ FRACTION SLASH
    2 DIGIT TWO
    >>> four_squared = '4²'
    >>> normalize('NFKC', four_squared)
    '42'
    >>> micro = 'μ'
    >>> micro_kc = normalize('NFKC', micro)
    >>> micro, micro_kc
    ('μ', 'μ')
    >>> ord(micro), ord(micro_kc)
    (181, 956)
    >>> name(micro), name(micro_kc)
    ('MICRO SIGN', 'GREEK SMALL LETTER MU')

4.6.1. Case Folding

大小写折叠(case folding)本质上就是把所有文本改成小写, 但有一些额外转换. 通过str.casefold()方法实现. 如果字符串所有的字符都是latin1字符, 那么s.casefold()s.lower()`产生相同的结果. 只有两个例外

  1. micro sign µ 被转成小写希腊字母mu(虽然在大多数字体下, 他们看起来长得一样).
  2. 德文字母Eszett也就是'sharp s'(ß)被转成'ss'
  3. 大概有300个码点, str.casefold()str.lower()返回不同的结果.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> name(micro_cf)
'GREEK SMALL LETTER MU'
>>> micro = '\N{MICRO SIGN}'
>>> name(micro)
'MICRO SIGN'
>>> mu = micro.casefold()
>>> name(mu)
'GREEK SMALL LETTER MU'
>>> micro, mu
('µ', 'μ')
>>> eszett = 'ß'
>>> name(eszett)
'LATIN SMALL LETTER SHARP S'
>>> eszett_cf = eszett.casefold()
>>> eszett, eszett_cf
('ß', 'ss')

我理解大小写折叠的目的还是为了比较, 有些语言里一个大写字母对应多个小写字母, 所以折叠会把所有小写字母转换成统一的唯一小写字母.

4.6.2. Utility Functions for Normalized Text Matching

NFC和NFD规范化可以用于安全的比较大小写敏感的字符串. 大小写折叠可以帮助大小写不敏感的字符串. 下面两个是实用函数.

1
2
3
4
5
6
from unicodedata import normalize
def nfc_equal(str1, str2):
return normalize('NFC', str1) == normalize('NFC', str2)
def fold_equal(str1, str2):
return (normalize('NFC', str1).casefold() ==
normalize('NFC', str2).casefold())

4.6.3. Extreme “Normalization”: Taking Out Diacritics

  1. 去掉变音符号的的极端规范化, 见下面函数shave_marks
  2. 只对拉丁字母去掉变音符号, 见下面函数shave_marks_latin
  3. 更激进的一个步骤是把西方文本的通用符号替换成ASCII的等效字符串, 见下面函数asciize

4.7. Sorting Unicode Text

比较字符串默认是比较码点, 但这对于很多非ASCII字符并不是我们期望的结果. 解决方案是locale.strxfrm.

1
2
3
4
5
6
import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)

有几点需要注意:

  1. setlocale是个全局调用, 不建议在库中调用.
  2. 相应的locale必须安装在操作系统.
  3. locale名字必须拼写正确.
  4. 作者在linux和macOS尝试, 但并不是每次都成功.

4.7.1. Sorting with the Unicode Collation Algorithm

使用module pyuca.

1
2
3
4
5
6
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

4.8. The Unicode Database

Unicode标准提供了一个完整的数据库, 不仅包括码点和字符名子, 还包括字符的元数据. 例如, 一个字符是否是字母, 是否是可打印的, 是否是十进制的数字还是其他数学符号. 这就是strisalpha, isprintable, isdecimal, isnumeric等方法的工作原理. str.casefold也是使用这些信息.

4.8.1. Finding Characters by Name

下面是一个通过名字查找字符的小工具.

4.8.2. Numeric Meaning of Characters

下面这个例子展示了如何确定一个字符是否是数字的几种方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
import unicodedata
import re
re_digit = re.compile(r'\d')
sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'
for char in sample:
print(f'U+{ord(char):04x}',
char.center(6),
're_dig' if re_digit.match(char) else '-',
'isdig' if char.isdigit() else '-',
'isnum' if char.isnumeric() else '-',
f'{unicodedata.numeric(char):5.2f}',
unicodedata.name(char),
sep='\t')

下面就是输出结果.

4.9. Dual-Mode str and bytes APIs

有些标准库有函数可以接收strbytes参数, 然后根据不同类型的参数做不同的处理. 主要是在reos模块.

4.9.1 str Versus bytes in Regular Expressions

  1. 正则表达式可以被用在strbytes上, 但是如果用在bytes上, ASCII以外的字节, 既不被认为是数字, 也不被认为是文字.
  2. 对于str, 可以用re.ASCII标志使, , , , .

4.9.2 str Versus bytes in os Functions

所有跟os的函数都接受str或者bytes类型的文件名作为参数. 如果输入时str类型, 会根据sys.getfilesystemencoding()对输入字符串进行编码. 返回的也会是str类型.

如果输入bytes类型的文件名, 则返回的也是bytes.

5. Chapter 5. Data Class Builders

Python提供了几个方式可以创建作为域(field)的集合的简单类. 这种模式的类被叫做"数据类"(data class). 这一章会介绍3个不同的构造数据类的方式:

  1. collections.namedtuple, 最简单的方式, Python2.6引入.
  2. typing.NamedTuple, 需要类型提示(type hint)的另一个方式, python3.5引入, python3.6增加了一些语法.
  3. @dataclass.dataclass, 一个类装饰器, 可定制性加强, 增加了很选项和可能的复杂度. Python3.7引入.

这章大概看了看, 不是很感兴趣, 没啥难度, 不做笔记了, 需要用的时候再来翻阅.

6. Chapter 6. Object References, Mutability, and Recycling

本章新内容很少, 浏览了一下, 不写笔记了.

Part II. Functions as Objects

7. Chapter 7. Functions as First-Class Objects

程序语言里, First-class被定义为:

  1. 运行时创建
  2. 可以被赋值给一个数据结构里的变量
  3. 可以作为参数传给一个函数
  4. 可以作为一个结果从一个函数返回

7.1. Treating a Function Like an Object

几个无用的知识点:

  1. 运行时定义的语法
  2. __doc__
  3. type(fun)可以看出fun是一个funciton类的实例

7.2. Higher-Order Functions

以函数为参数的函数, 或者以函数作为返回值的函数, 可以被称作高阶函数(Higher-Order Functions)

介绍了几个built-in的高阶函数:

  1. sorted(iterable, /, *, key=None, reverse=False), 其中key是接收一个参数的函数, 其返回值值作为排序的关键字.
  2. map(function, iterable, *iterables), 第一个参数是目标函数, 作用是以后面的可迭代对象的每一个元素作为参数来调用这个目标函数, 返回一个以目标函数的返回值为元素的可迭代对象. 如果有额外的可迭代对象作为map的参数, 那么目标函数必须接受跟所有可迭代对象数量相等的位置参数, 每次执行迭代的时候取每个参数的一个元素为目标函数的参数. 当任何一个可迭代对象执行完所有元素时, 停止迭代.
  3. filter(function, iterable), 过滤出可迭代对象里让目标函数为真的元素组成一个新的可迭代函数. itertools.filterfalse()过滤出使目标函数返回假的元素.
  4. functools.reduce(function, iterable[, initializer]), 基本上等于下面的逻辑.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    def reduce(function, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
    value = next(it)
    else:
    value = initializer
    for element in it:
    value = function(value, element)
    return value

自从引入了list推导式(comprehensions)和生成器表达式(generator expressions)

7.3 Anonymous Functions

lamda表达式可以用一个表达式(expression)创建一个匿名函数. 这里的限制就是无法使用语句(statement). 最佳的使用匿名函数的地方就是在高阶函数的参数列表中使用.

如果您发现一段代码因为lambda而难以理解, Fredrik Lundh建议采用以下重构过程:

  1. 写一条comment解释lambda到底做了什么.
  2. 研究一下comment, 想出一个能描述comment本质的名字.
  3. 使用该名称将lambda转换为def语句.
  4. 删除评论.

这些步骤引用自必读的"函数式编程指南".

7.4 The Nine Flavors of Callable Objects

9个可调用对象

  1. User-defined functions: def语句和lambda表达式
  2. Built-in functions
  3. Built-in methods
  4. Methods: 定义在class语句内的函数
  5. Classes: 当被调用时, 一个类先执行__new__方法创建一个实例对象, 然后调用__init__初始化这个对象. Python没有new操作符, 所以调用一个类就跟调用一个函数一样
  6. Class instance: 如果一个类定义了__call__方法, 那么类实例可以像函数一样调用.
  7. Generator functions: 在函数体内使用了yield关键字的函数或方法, 在调用时返回一个生成器(generator).
  8. Native coroutine function: 函数或方法用async def定义的, 当调用的时候返回一个协程(coroutine). Python3.5加入.
  9. Asynchronous generator functions: 函数或方法用async def定义, 并且函数体使用了yield语句. 当被调用时生成一个异步生成器. 可以用于async for语句. Python3.6加入.

生成器在17章讨论, 协程和异步生成器在21章讨论. 判断一个对象是否可调用使用内置的callable.

7.5. User-Defined Callable Types

跳过, 无笔记

7.6. From Positional to Keyword-Only Parameters

例子:

1
def fun(a,b,/,c,d,*,e,f)

'/'前的参数只能是位置参数, '*'后的参数只能是关键字参数, 中间的两者都可以是.

7.7. Packages for Functional Programming

7.7.1. The operator Module

Operator模块, 提供了函数编程的各种运算符, 其中两个比较特殊, 一个是取元素, 一个是属性访问.

  1. itemgetter,

    1
    2
    After f = itemgetter(2), the call f(r) returns r[2].
    After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3]).

  2. attrgetter,

    1
    2
    3
    After f = attrgetter('name'), the call f(b) returns b.name.
    After f = attrgetter('name', 'date'), the call f(b) returns (b.name, b.date).
    After f = attrgetter('name.first', 'name.last'), the call f(b) returns (b.name.first, b.name.last).

7.7.2. Freezing Arguments with functools.partial

1
2
functools.partial(func, /, *args, **keywords)
functools.partialmethod(func, /, *args, **keywords)

可以锁定函数或者方法的部分参数. 等效的逻辑差不多如下:

1
2
3
4
5
6
7
8
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

8. Chapter 8. Type Hints in Functions

作者认为类型提示(Type Hint)是自2001年来Python最大的变化. 但类型提示是可选的. PEP-484提供了从语法(syntax)和语义(semantics)上, 明确定义变量的类型, 包括函数参数, 返回值, 和其他变量. 目标是帮助开发工具通过对代码的静态分析发现bugs.

8.1. About Gradual Typing

PEP-484给python引进了一个类型系统 gradual type system(不好翻译, 就叫渐近类型吧). 其他使用渐近类型系统的语言包括Microsoft TypeScript, Dart, Hack.

一个渐近类型系统:

  1. 是可选的
  2. 不在运行时获取类型错误
  3. 不增强性能

8.2. Types Are Defined by Supported Operations

以下内容来自于PEP-483得翻译:

  1. 类型(Type)就是一些值得集合, 以及一些能够应用这些值得函数得集合.
  2. 子类型(Subtype)关系, b类型的值\(\subset\)a类型的值, a类型的函数\(\subset\)b类型的函数, 称b类型是a类型的子类型. 此时赋值语句"a类型的变量 = b类型的变量"是安全的.
  3. 一致性(consistent)关系:
    • 类型t1是类型t2的子类型, 那么t1跟t2是一致的. (反过来不成立)
    • Any跟任何类型都是一致的.
    • 任何类型跟Any都是一致的.
  4. 类(class)是通过class语句定义的对象工厂, 并由内置函数type(obj)返回. 类是一个动态的运行时概念.

未完待续