流畅的 Python 第二版(GPT 重译)(二)(1)https://developer.aliyun.com/article/1484408
dict 的变体
本节概述了标准库中包含的映射类型,除了已在“defaultdict: Another Take on Missing Keys”中介绍的defaultdict
。
collections.OrderedDict
自从 Python 3.6 开始,内置的dict
也保持了键的有序性,使用OrderedDict
的最常见原因是编写与早期 Python 版本向后兼容的代码。话虽如此,Python 的文档列出了dict
和OrderedDict
之间的一些剩余差异,我在这里引用一下——只重新排列项目以便日常使用:
OrderedDict
的相等操作检查匹配的顺序。OrderedDict
的popitem()
方法具有不同的签名。它接受一个可选参数来指定要弹出的项目。OrderedDict
有一个move_to_end()
方法,可以高效地将一个元素重新定位到末尾。- 常规的
dict
被设计为在映射操作方面非常出色。跟踪插入顺序是次要的。 OrderedDict
被设计为在重新排序操作方面表现良好。空间效率、迭代速度和更新操作的性能是次要的。- 从算法上讲,
OrderedDict
比dict
更擅长处理频繁的重新排序操作。这使得它适用于跟踪最近的访问(例如,在 LRU 缓存中)。
collections.ChainMap
ChainMap
实例保存了一个可以作为一个整体搜索的映射列表。查找是按照构造函数调用中出现的顺序在每个输入映射上执行的,并且一旦在这些映射中的一个中找到键,查找就成功了。例如:
>>> 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
ChainMap
实例不会复制输入映射,而是保留对它们的引用。对ChainMap
的更新或插入只会影响第一个输入映射。继续上一个例子:
>>> chain['c'] = -1 >>> d1 {'a': 1, 'b': 3, 'c': -1} >>> d2 {'a': 2, 'b': 4, 'c': 6}
ChainMap
对于实现具有嵌套作用域的语言的解释器非常有用,其中每个映射表示一个作用域上下文,从最内部的封闭作用域到最外部作用域。collections
文档中的“ChainMap objects”部分有几个ChainMap
使用示例,包括这个受 Python 变量查找基本规则启发的代码片段:
import builtins pylookup = ChainMap(locals(), globals(), vars(builtins))
示例 18-14 展示了一个用于实现 Scheme 编程语言子集解释器的ChainMap
子类。
collections.Counter
一个为每个键保存整数计数的映射。更新现有键会增加其计数。这可用于计算可散列对象的实例数量或作为多重集(稍后在本节讨论)。Counter
实现了 +
和 -
运算符来组合计数,并提供其他有用的方法,如 most_common([n])
,它返回一个按顺序排列的元组列表,其中包含 n 个最常见的项目及其计数;请参阅文档。这里是 Counter
用于计算单词中的字母:
>>> 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)]
请注意,'b'
和 'r'
键并列第三,但 ct.most_common(3)
只显示了三个计数。
要将 collections.Counter
用作多重集,假装每个键是集合中的一个元素,计数是该元素在集合中出现的次数。
shelve.Shelf
标准库中的 shelve
模块为字符串键到以 pickle
二进制格式序列化的 Python 对象的映射提供了持久存储。当你意识到 pickle 罐子存放在架子上时,shelve
这个奇怪的名字就有了意义。
shelve.open
模块级函数返回一个 shelve.Shelf
实例——一个简单的键-值 DBM 数据库,由 dbm
模块支持,具有以下特点:
shelve.Shelf
是abc.MutableMapping
的子类,因此它提供了我们期望的映射类型的基本方法。- 此外,
shelve.Shelf
提供了一些其他的 I/O 管理方法,如sync
和close
。 Shelf
实例是一个上下文管理器,因此您可以使用with
块来确保在使用后关闭它。- 每当将新值分配给键时,键和值都会被保存。
- 键必须是字符串。
- 值必须是
pickle
模块可以序列化的对象。
shelve、dbm 和 pickle 模块的文档提供了更多细节和一些注意事项。
警告
Python 的 pickle
在最简单的情况下很容易使用,但也有一些缺点。在采用涉及 pickle
的任何解决方案之前,请阅读 Ned Batchelder 的“Pickle 的九个缺陷”。在他的帖子中,Ned 提到了其他要考虑的序列化格式。
OrderedDict
、ChainMap
、Counter
和 Shelf
都可以直接使用,但也可以通过子类化进行自定义。相比之下,UserDict
只是作为一个可扩展的基类。
通过继承 UserDict
而不是 dict
来创建新的映射类型
最好通过扩展 collections.UserDict
来创建新的映射类型,而不是 dict
。当我们尝试扩展我们的 StrKeyDict0
(来自示例 3-8)以确保将任何添加到映射中的键存储为 str
时,我们意识到这一点。
更好地通过子类化 UserDict
而不是 dict
的主要原因是,内置类型有一些实现快捷方式,最终迫使我们覆盖我们可以从 UserDict
继承而不会出现问题的方法。⁷
请注意,UserDict
不继承自 dict
,而是使用组合:它有一个内部的 dict
实例,称为 data
,用于保存实际的项目。这避免了在编写特殊方法如 __setitem__
时出现不必要的递归,并简化了 __contains__
的编写,与示例 3-8 相比更加简单。
由于 UserDict
的存在,StrKeyDict
(示例 3-9)比 StrKeyDict0
(示例 3-8)更简洁,但它做得更多:它将所有键都存储为 str
,避免了如果实例被构建或更新时包含非字符串键时可能出现的令人不快的情况。
示例 3-9. StrKeyDict
在插入、更新和查找时总是将非字符串键转换为 str
。
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 # ④
①
StrKeyDict
扩展了 UserDict
。
②
__missing__
与示例 3-8 中的一样。
③
__contains__
更简单:我们可以假定所有存储的键都是 str
,并且可以在 self.data
上进行检查,而不是像在 StrKeyDict0
中那样调用 self.keys()
。
④
__setitem__
将任何 key
转换为 str
。当我们可以委托给 self.data
属性时,这种方法更容易被覆盖。
因为 UserDict
扩展了 abc.MutableMapping
,使得使 StrKeyDict
成为一个完整的映射的剩余方法都是从 UserDict
、MutableMapping
或 Mapping
继承的。尽管后者是抽象基类(ABC),但它们有几个有用的具体方法。以下方法值得注意:
MutableMapping.update
这种强大的方法可以直接调用,但也被 __init__
用于从其他映射、从 (key, value)
对的可迭代对象和关键字参数加载实例。因为它使用 self[key] = value
来添加项目,所以最终会调用我们的 __setitem__
实现。
Mapping.get
在 StrKeyDict0
(示例 3-8)中,我们不得不编写自己的 get
来返回与 __getitem__
相同的结果,但在 示例 3-9 中,我们继承了 Mapping.get
,它的实现与 StrKeyDict0.get
完全相同(请参阅 Python 源代码)。
提示
安托万·皮特鲁(Antoine Pitrou)撰写了 PEP 455—向 collections 添加一个键转换字典 和一个增强 collections
模块的补丁,其中包括一个 TransformDict
,比 StrKeyDict
更通用,并保留提供的键,然后应用转换。PEP 455 在 2015 年 5 月被拒绝—请参阅雷蒙德·赫廷格的 拒绝消息。为了尝试 TransformDict
,我从 issue18986 中提取了皮特鲁的补丁,制作成了一个独立的模块(03-dict-set/transformdict.py 在 Fluent Python 第二版代码库 中)。
我们知道有不可变的序列类型,但不可变的映射呢?在标准库中确实没有真正的不可变映射,但有一个替代品可用。接下来是。
不可变映射
标准库提供的映射类型都是可变的,但您可能需要防止用户意外更改映射。再次在硬件编程库中找到一个具体的用例,比如 Pingo,在 “缺失方法” 中提到:board.pins
映射表示设备上的物理 GPIO 引脚。因此,防止意外更新 board.pins
是有用的,因为硬件不能通过软件更改,所以映射的任何更改都会使其与设备的物理现实不一致。
types
模块提供了一个名为 MappingProxyType
的包装类,给定一个映射,它返回一个 mappingproxy
实例,这是原始映射的只读但动态代理。这意味着可以在 mappingproxy
中看到对原始映射的更新,但不能通过它进行更改。参见 示例 3-10 进行简要演示。
示例 3-10. MappingProxyType
从 dict
构建一个只读的 mappingproxy
实例。
>>> 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 # ③ mappingproxy({1: 'A', 2: 'B'}) >>> d_proxy[2] 'B' >>>
①
d
中的项目可以通过 d_proxy
看到。
②
不能通过 d_proxy
进行更改。
③
d_proxy
是动态的:d
中的任何更改都会反映出来。
在硬件编程场景中,这个方法在实践中可以这样使用:具体的 Board
子类中的构造函数会用 pin 对象填充一个私有映射,并通过一个实现为 mappingproxy
的公共 .pins
属性将其暴露给 API 的客户端。这样,客户端就无法意外地添加、删除或更改 pin。
接下来,我们将介绍视图—它允许在 dict
上进行高性能操作,而无需不必要地复制数据。
字典视图
dict
实例方法.keys()
、.values()
和.items()
返回类dict_keys
、dict_values
和dict_items
的实例,分别。这些字典视图是dict
实现中使用的内部数据结构的只读投影。它们避免了等效 Python 2 方法的内存开销,这些方法返回了重复数据的列表,这些数据已经在目标dict
中,它们还替换了返回迭代器的旧方法。
示例 3-11 展示了所有字典视图支持的一些基本操作。
示例 3-11。.values()
方法返回字典中值的视图
>>> d = dict(a=10, b=20, c=30) >>> values = d.values() >>> values dict_values([10, 20, 30]) # ① >>> len(values) # ② 3 >>> list(values) # ③ [10, 20, 30] >>> reversed(values) # ④ <dict_reversevalueiterator object at 0x10e9e7310> >>> values[0] # ⑤ Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'dict_values' object is not subscriptable
①
视图对象的repr
显示其内容。
②
我们可以查询视图的len
。
③
视图是可迭代的,因此很容易从中创建列表。
④
视图实现了__reversed__
,返回一个自定义迭代器。
⑤
我们不能使用[]
从视图中获取单个项目。
视图对象是动态代理。如果源dict
被更新,您可以立即通过现有视图看到更改。继续自示例 3-11:
>>> d['z'] = 99 >>> d {'a': 10, 'b': 20, 'c': 30, 'z': 99} >>> values dict_values([10, 20, 30, 99])
类dict_keys
、dict_values
和dict_items
是内部的:它们不通过__builtins__
或任何标准库模块可用,即使你获得了其中一个的引用,也不能在 Python 代码中从头开始创建视图:
>>> values_class = type({}.values()) >>> v = values_class() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: cannot create 'dict_values' instances
dict_values
类是最简单的字典视图——它只实现了__len__
、__iter__
和__reversed__
特殊方法。除了这些方法,dict_keys
和dict_items
实现了几个集合方法,几乎和frozenset
类一样多。在我们讨论集合之后,我们将在“字典视图上的集合操作”中更多地谈到dict_keys
和dict_items
。
现在让我们看一些由dict
在幕后实现的规则和提示。
dict
工作方式的实际后果
Python 的dict
的哈希表实现非常高效,但重要的是要了解这种设计的实际影响:
- 键必须是可散列的对象。它们必须实现适当的
__hash__
和__eq__
方法,如“什么是可散列”中所述。 - 通过键访问项目非常快速。一个
dict
可能有数百万个键,但 Python 可以通过计算键的哈希码并推导出哈希表中的索引偏移量直接定位一个键,可能会有少量尝试来找到匹配的条目的开销。 - 键的顺序保留是 CPython 3.6 中
dict
更紧凑的内存布局的副作用,在 3.7 中成为官方语言特性。 - 尽管其新的紧凑布局,字典不可避免地具有显着的内存开销。对于容器来说,最紧凑的内部数据结构将是一个指向项目的指针数组。⁸ 相比之下,哈希表需要存储更多的数据,而 Python 需要保持至少三分之一的哈希表行为空以保持高效。
- 为了节省内存,避免在
__init__
方法之外创建实例属性。
最后一条关于实例属性的提示来自于 Python 的默认行为是将实例属性存储在一个特殊的__dict__
属性中,这是一个附加到每个实例的dict
。自从 Python 3.3 实现了PEP 412—Key-Sharing Dictionary以来,一个类的实例可以共享一个与类一起存储的公共哈希表。当__init__
返回时,具有相同属性名称的每个新实例的__dict__
都共享该公共哈希表。然后,每个实例的__dict__
只能保存自己的属性值作为指针的简单数组。在__init__
之后添加一个实例属性会强制 Python 为__dict__
创建一个新的哈希表,用于该实例的__dict__
(这是 Python 3.3 之前所有实例的默认行为)。根据 PEP 412,这种优化可以减少面向对象程序的内存使用量 10%至 20%。
紧凑布局和键共享优化的细节相当复杂。更多信息,请阅读fluentpython.com上的“集合和字典的内部”。
现在让我们深入研究集合。
集合理论
在 Python 中,集合并不新鲜,但仍然有些被低估。set
类型及其不可变的姊妹frozenset
首次出现在 Python 2.3 标准库中作为模块,并在 Python 2.6 中被提升为内置类型。
注意
在本书中,我使用“集合”一词来指代set
和frozenset
。当专门讨论set
类型,我使用等宽字体:set
。
集合是一组唯一对象。一个基本用例是去除重复项:
>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs'] >>> set(l) {'eggs', 'spam', 'bacon'} >>> list(set(l)) ['eggs', 'spam', 'bacon']
提示
如果你想去除重复项但又保留每个项目的第一次出现的顺序,你现在可以使用一个普通的dict
来实现,就像这样:
>>> dict.fromkeys(l).keys() dict_keys(['spam', 'eggs', 'bacon']) >>> list(dict.fromkeys(l).keys()) ['spam', 'eggs', 'bacon']
集合元素必须是可散列的。set
类型不可散列,因此你不能用嵌套的set
实例构建一个set
。但是frozenset
是可散列的,所以你可以在set
中包含frozenset
元素。
除了强制唯一性外,集合类型还实现了许多集合操作作为中缀运算符,因此,给定两个集合a
和b
,a | b
返回它们的并集,a & b
计算交集,a - b
表示差集,a ^ b
表示对称差。巧妙地使用集合操作可以减少 Python 程序的行数和执行时间,同时使代码更易于阅读和理解——通过消除循环和条件逻辑。
例如,想象一下你有一个大型的电子邮件地址集合(haystack
)和一个较小的地址集合(needles
),你需要计算needles
在haystack
中出现的次数。由于集合交集(&
运算符),你可以用一行代码实现这个功能(参见示例 3-12)。
示例 3-12. 计算在一个集合中针的出现次数,两者都是集合类型
found = len(needles & haystack)
没有交集运算符,你将不得不编写示例 3-13 来完成与示例 3-12 相同的任务。
示例 3-13. 计算在一个集合中针的出现次数(与示例 3-12 的结果相同)
found = 0 for n in needles: if n in haystack: found += 1
示例 3-12 比示例 3-13 运行速度稍快。另一方面,示例 3-13 适用于任何可迭代对象needles
和haystack
,而示例 3-12 要求两者都是集合。但是,如果你手头没有集合,你可以随时动态构建它们,就像示例 3-14 中所示。
示例 3-14. 计算在一个集合中针的出现次数;这些行适用于任何可迭代类型
found = len(set(needles) & set(haystack)) # another way: found = len(set(needles).intersection(haystack))
当然,在构建示例 3-14 中的集合时会有额外的成本,但如果needles
或haystack
中的一个已经是一个集合,那么示例 3-14 中的替代方案可能比示例 3-13 更便宜。
任何前述示例中的一个都能在haystack
中搜索 1,000 个元素,其中包含 10,000,000 个项目,大约需要 0.3 毫秒,即每个元素接近 0.3 微秒。
除了极快的成员测试(由底层哈希表支持),set
和 frozenset
内置类型提供了丰富的 API 来创建新集合或在set
的情况下更改现有集合。我们将很快讨论这些操作,但首先让我们谈谈语法。
集合字面量
set
字面量的语法—{1}
,{1, 2}
等—看起来与数学符号一样,但有一个重要的例外:没有空set
的字面表示,因此我们必须记得写set()
。
语法怪癖
不要忘记,要创建一个空的set
,应该使用没有参数的构造函数:set()
。如果写{}
,你将创建一个空的dict
—在 Python 3 中这一点没有改变。
在 Python 3 中,集合的标准字符串表示总是使用{…}
符号,除了空集:
>>> s = {1} >>> type(s) <class 'set'> >>> s {1} >>> s.pop() 1 >>> s set()
字面set
语法如{1, 2, 3}
比调用构造函数(例如,set([1, 2, 3])
)更快且更易读。后一种形式较慢,因为要评估它,Python 必须查找set
名称以获取构造函数,然后构建一个列表,最后将其传递给构造函数。相比之下,要处理像{1, 2, 3}
这样的字面量,Python 运行一个专门的BUILD_SET
字节码。¹⁰
没有特殊的语法来表示frozenset
字面量—它们必须通过调用构造函数创建。在 Python 3 中的标准字符串表示看起来像一个frozenset
构造函数调用。请注意控制台会话中的输出:
>>> frozenset(range(10)) frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
谈到语法,列表推导的想法也被用来构建集合。
集合推导式
集合推导式(setcomps)在 Python 2.7 中添加,与我们在“dict 推导式”中看到的 dictcomps 一起。示例 3-15 展示了如何。
示例 3-15. 构建一个拉丁-1 字符集,其中 Unicode 名称中包含“SIGN”一词
>>> from unicodedata import name # ① >>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} # ② {'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©', '°', '+', '÷', '±', '>', '¬', '®', '%'}
①
从unicodedata
导入name
函数以获取字符名称。
②
构建字符集,其中字符代码从 32 到 255,名称中包含 'SIGN'
一词。
输出的顺序会因为“什么是可哈希的”中提到的盐哈希而对每个 Python 进程进行更改。
语法问题在一边,现在让我们考虑集合的行为。
集合工作方式的实际后果
set
和 frozenset
类型都是使用哈希表实现的。这会产生以下影响:
- 集合元素必须是可哈希对象。它们必须实现适当的
__hash__
和__eq__
方法,如“什么是可哈希的”中所述。 - 成员测试非常高效。一个集合可能有数百万个元素,但可以通过计算其哈希码并推导出索引偏移量来直接定位一个元素,可能需要少量尝试来找到匹配的元素或耗尽搜索。
- 与低级数组指针相比,集合具有显着的内存开销—后者更紧凑但搜索超过少量元素时也更慢。
- 元素顺序取决于插入顺序,但并不是以有用或可靠的方式。如果两个元素不同但具有相同的哈希码,则它们的位置取决于哪个元素先添加。
- 向集合添加元素可能会改变现有元素的顺序。这是因为如果哈希表超过三分之二满,算法会变得不那么高效,因此 Python 可能需要在增长时移动和调整表格。当发生这种情况时,元素将被重新插入,它们的相对顺序可能会改变。
详细信息请参见“集合和字典的内部”在fluentpython.com。
现在让我们来看看集合提供的丰富操作。
集合操作
| | | s.difference(it, …)
| s
和从可迭代对象 it
构建的所有集合的差集 |
| S ⊆ Z | s <= z
| s.__le__(z)
| s
是 z
集合的子集 |
费曼学习法的灵感源于理查德·费曼,这位物理学诺贝尔奖得主。
| | | | |
| 数学符号 | Python 运算符 | 方法 | 描述 |
s.intersection(it, …) |
s 和从可迭代对象 it 构建的所有集合的交集 |
||
s -= z |
s.__isub__(z) |
s 更新为 s 和 z 的差集 |
|
S \ Z | s - z |
s.__sub__(z) |
s 和 z 的相对补集或差集 |
z ^ s |
s.__rxor__(z) |
反转 ^ 运算符 |
|
s.difference_update(it, …) |
s 更新为 s 和从可迭代对象 it 构建的所有集合的差集 |
||
s &= z |
s.__iand__(z) |
s 更新为 s 和 z 的交集 |
|
s.union(it, …) |
s 和从可迭代对象 it 构建的所有集合的并集 |
||
图 3-2 概述了可变和不可变集合上可用的方法。其中许多是重载运算符的特殊方法,如 & 和 >= 。表 3-2 显示了在 Python 中具有对应运算符或方法的数学集合运算符。请注意,一些运算符和方法会对目标集合进行就地更改(例如 &= ,difference_update 等)。这样的操作在数学集合的理想世界中毫无意义,并且在 frozenset 中未实现。 |
|||
s.update(it, …) |
s 更新为 s 和从可迭代对象 it 构建的所有集合的并集 |
||
z & s |
s.__rand__(z) |
反转 & 运算符 |
|
S ∆ Z | s ^ z |
s.__xor__(z) |
对称差集(s & z 的补集) |
表 3-2. 数学集合操作:这些方法要么生成新集合,要么在原地更新目标集合(如果可变) | |||
S ∩ Z = ∅ | s.isdisjoint(z) |
s 和 z 互不相交(没有共同元素) |
|
s.symmetric_difference(it) |
s & set(it) 的补集 |
||
S ∩ Z | s & z |
s.__and__(z) |
s 和 z 的交集 |
使用费曼的技巧,你可以在短短20 min 内深入理解知识点,而且记忆深刻,难以遗忘。 |
|||
s ^= z |
s.__ixor__(z) |
s 更新为 s 和 z 的对称差集 |
|
S ∪ Z | s | z |
s.__or__(z) |
s 和 z 的并集 |
e ∈ S | e in s |
s.__contains__(e) |
元素 e 是 s 的成员 |
s.intersection_update(it, …) |
s 更新为 s 和从可迭代对象 it 构建的所有集合的交集 |
||
数学符号 | Python 运算符 | 方法 | 描述 |
表 3-3. 返回布尔值的集合比较运算符和方法 | |||
s.symmetric_difference_update(it, …) |
s 更新为 s 和从可迭代对象 it 构建的所有集合的对称差 |
提示
| | | | |
| 表 3-3 列出了集合谓词:返回 True
或 False
的运算符和方法。 |
— | — | — | — |
图 3-2. MutableSet 及其来自 collections.abc 的超类的简化 UML 类图(斜体名称为抽象类和抽象方法;为简洁起见省略了反转运算符方法) |
|||
z | s |
s.__ror__(z) |
反转 | 运算符 |
|
z - s |
s.__rsub__(z) |
反转 - 运算符 |
|
s |= z |
s.__ior__(z) |
s 更新为 s 和 z 的并集 |
|
s.issubset(it) |
s 是从可迭代对象 it 构建的集合的子集 |
||
S ⊂ Z | s < z |
s.__lt__(z) |
s 是 z 集合的真子集 |
S ⊇ Z | s >= z |
s.__ge__(z) |
s 是 z 集合的超集 |
s.issuperset(it) |
s 是从可迭代对象 it 构建的集合的超集 |
||
S ⊃ Z | s > z |
s.__gt__(z) |
s 是 z 集合的真超集 |
除了从数学集合理论中派生的运算符和方法外,集合类型还实现了其他实用的方法,总结在表 3-4 中。
表 3-4. 额外的集合方法
集合 | 冻结集合 | ||
s.add(e) |
● | 向 s 添加元素 e |
|
s.clear() |
● | 移除 s 的所有元素 |
|
s.copy() |
● | ● | s 的浅复制 |
s.discard(e) |
● | 如果存在则从 s 中移除元素 e |
|
s.__iter__() |
● | ● | 获取 s 的迭代器 |
s.__len__() |
● | ● | len(s) |
s.pop() |
● | 从 s 中移除并返回一个元素,如果 s 为空则引发 KeyError |
|
s.remove(e) |
● | 从 s 中移除元素 e ,如果 e 不在 s 中则引发 KeyError |
这完成了我们对集合特性的概述。如“字典视图”中承诺的,我们现在将看到两种字典视图类型的行为非常类似于 frozenset
。
字典视图上的集合操作
表 3-5 显示了由 dict
方法 .keys()
和 .items()
返回的视图对象与 frozenset
非常相似。
表 3-5. frozenset
、dict_keys
和 dict_items
实现的方法
冻结集合 | dict_keys | dict_items | 描述 | |
s.__and__(z) |
● | ● | ● | s & z (s 和 z 的交集) |
s.__rand__(z) |
● | ● | ● | 反转 & 运算符 |
s.__contains__() |
● | ● | ● | e in s |
s.copy() |
● | s 的浅复制 |
||
s.difference(it, …) |
● | s 和可迭代对象 it 等的差集 |
||
s.intersection(it, …) |
● | s 和可迭代对象 it 等的交集 |
||
s.isdisjoint(z) |
● | ● | ● | s 和 z 不相交(没有共同元素) |
s.issubset(it) |
● | s 是可迭代对象 it 的子集 |
||
s.issuperset(it) |
● | s 是可迭代对象 it 的超集 |
||
s.__iter__() |
● | ● | ● | 获取 s 的迭代器 |
s.__len__() |
● | ● | ● | len(s) |
s.__or__(z) |
● | ● | ● | s | z (s 和 z 的并集) |
s.__ror__() |
● | ● | ● | 反转 | 运算符 |
s.__reversed__() |
● | ● | 获取 s 的反向迭代器 |
|
s.__rsub__(z) |
● | ● | ● | 反转 - 运算符 |
s.__sub__(z) |
● | ● | ● | s - z (s 和 z 之间的差集) |
s.symmetric_difference(it) |
● | s & set(it) 的补集 |
||
s.union(it, …) |
● | s 和可迭代对象 it 等的并集 |
||
s.__xor__() |
● | ● | ● | s ^ z (s 和 z 的对称差集) |
s.__rxor__() |
● | ● | ● | 反转 ^ 运算符 |
特别地,dict_keys
和 dict_items
实现了支持强大的集合运算符 &
(交集)、|
(并集)、-
(差集)和 ^
(对称差集)的特殊方法。
例如,使用 &
很容易获得出现在两个字典中的键:
>>> d1 = dict(a=1, b=2, c=3, d=4) >>> d2 = dict(b=20, d=40, e=50) >>> d1.keys() & d2.keys() {'b', 'd'}
请注意 &
的返回值是一个 set
。更好的是:字典视图中的集合运算符与 set
实例兼容。看看这个:
>>> s = {'a', 'e', 'i'} >>> d1.keys() & s {'a'} >>> d1.keys() | s {'a', 'c', 'b', 'd', 'i', 'e'}
警告
一个 dict_items
视图仅在字典中的所有值都是可哈希的情况下才能作为集合使用。尝试在具有不可哈希值的 dict_items
视图上进行集合操作会引发 TypeError: unhashable type 'T'
,其中 T
是有问题值的类型。
另一方面,dict_keys
视图始终可以用作集合,因为每个键都是可哈希的—按定义。
使用视图和集合运算符将节省大量循环和条件语句,当检查代码中字典内容时,让 Python 在 C 中高效实现为您工作!
就这样,我们可以结束这一章了。
章节总结
字典是 Python 的基石。多年来,熟悉的 {k1: v1, k2: v2}
文字语法得到了增强,支持使用 **
、模式匹配以及 dict
推导式。
除了基本的 dict
,标准库还提供了方便、即用即用的专用映射,如 defaultdict
、ChainMap
和 Counter
,都定义在 collections
模块中。随着新的 dict
实现,OrderedDict
不再像以前那样有用,但应该保留在标准库中以保持向后兼容性,并具有 dict
没有的特定特性,例如在 ==
比较中考虑键的顺序。collections
模块中还有 UserDict
,一个易于使用的基类,用于创建自定义映射。
大多数映射中可用的两个强大方法是 setdefault
和 update
。setdefault
方法可以更新持有可变值的项目,例如在 list
值的 dict
中,避免为相同键进行第二次搜索。update
方法允许从任何其他映射、提供 (key, value)
对的可迭代对象以及关键字参数进行批量插入或覆盖项目。映射构造函数也在内部使用 update
,允许实例从映射、可迭代对象或关键字参数初始化。自 Python 3.9 起,我们还可以使用 |=
运算符更新映射,使用 |
运算符从两个映射的并集创建一个新映射。
映射 API 中一个巧妙的钩子是 __missing__
方法,它允许你自定义当使用 d[k]
语法(调用 __getitem__
)时找不到键时发生的情况。
collections.abc
模块提供了 Mapping
和 MutableMapping
抽象基类作为标准接口,对于运行时类型检查非常有用。types
模块中的 MappingProxyType
创建了一个不可变的外观,用于保护不希望意外更改的映射。还有用于 Set
和 MutableSet
的抽象基类。
字典视图是 Python 3 中的一个重要补充,消除了 Python 2 中 .keys()
、.values()
和 .items()
方法造成的内存开销,这些方法构建了重复数据的列表,复制了目标 dict
实例中的数据。此外,dict_keys
和 dict_items
类支持 frozenset
的最有用的运算符和方法。
进一步阅读
在 Python 标准库文档中,“collections—Container datatypes” 包括了几种映射类型的示例和实用配方。模块 Lib/collections/init.py 的 Python 源代码是任何想要创建新映射类型或理解现有映射逻辑的人的绝佳参考。David Beazley 和 Brian K. Jones 的 Python Cookbook, 3rd ed.(O’Reilly)第一章有 20 个方便而富有见地的数据结构配方,其中大部分使用 dict
以巧妙的方式。
Greg Gandenberger 主张继续使用 collections.OrderedDict
,理由是“显式胜于隐式”,向后兼容性,以及一些工具和库假定 dict
键的顺序是无关紧要的。他的帖子:“Python Dictionaries Are Now Ordered. Keep Using OrderedDict”。
PEP 3106—Revamping dict.keys(), .values() and .items() 是 Guido van Rossum 为 Python 3 提出字典视图功能的地方。在摘要中,他写道这个想法来自于 Java 集合框架。
PyPy是第一个实现 Raymond Hettinger 提出的紧凑字典建议的 Python 解释器,他们在“PyPy 上更快、更节省内存和更有序的字典”中发表了博客,承认 PHP 7 中采用了类似的布局,描述在PHP 的新哈希表实现中。当创作者引用先前的作品时,总是很棒。
在 PyCon 2017 上,Brandon Rhodes 介绍了“字典更强大”,这是他经典动画演示“强大的字典”的续集——包括动画哈希冲突!另一部更加深入的关于 Python dict
内部的视频是由 Raymond Hettinger 制作的“现代字典”,他讲述了最初未能向 CPython 核心开发人员推销紧凑字典的经历,他游说了 PyPy 团队,他们采纳了这个想法,这个想法得到了推广,并最终由 INADA Naoki 贡献给了 CPython 3.6,详情请查看Objects/dictobject.c中的 CPython 代码的详细注释和设计文档Objects/dictnotes.txt。
为了向 Python 添加集合的原因在PEP 218—添加内置集合对象类型中有记录。当 PEP 218 被批准时,没有采用特殊的文字语法来表示集合。set
文字是为 Python 3 创建的,并与dict
和set
推导一起回溯到 Python 2.7。在 PyCon 2019 上,我介绍了“集合实践:从 Python 的集合类型中学习”,描述了实际程序中集合的用例,涵盖了它们的 API 设计以及使用位向量而不是哈希表的整数元素的集合类uintset
的实现,灵感来自于 Alan Donovan 和 Brian Kernighan 的优秀著作The Go Programming Language第六章中的一个示例(Addison-Wesley)。
IEEE 的Spectrum杂志有一篇关于汉斯·彼得·卢恩的故事,他是一位多产的发明家,他申请了一项关于根据可用成分选择鸡尾酒配方的穿孔卡片盒的专利,以及其他包括…哈希表在内的多样化发明!请参阅“汉斯·彼得·卢恩和哈希算法的诞生”。
¹ 通过调用 ABC 的.register()
方法注册的任何类都是虚拟子类,如“ABC 的虚拟子类”中所解释的。如果设置了特定的标记位,通过 Python/C API 实现的类型也是合格的。请参阅Py_TPFLAGS_MAPPING
。
² Python 术语表中关于“可散列”的条目使用“哈希值”一词,而不是哈希码。我更喜欢哈希码,因为在映射的上下文中经常讨论这个概念,其中项由键和值组成,因此提到哈希码作为值可能会令人困惑。在本书中,我只使用哈希码。
³ 请参阅PEP 456—安全和可互换的哈希算法以了解安全性问题和采用的解决方案。
⁴ 原始脚本出现在 Martelli 的“重新学习 Python”演示的第 41 页中。他的脚本实际上是dict.setdefault
的演示,如我们的示例 3-5 所示。
⁵ 这是将方法作为一等函数使用的示例,是第七章的主题。
⁶ 其中一个库是Pingo.io,目前已不再进行活跃开发。
⁷ 关于子类化dict
和其他内置类型的确切问题在“子类化内置类型是棘手的”中有所涵盖。
⁸ 这就是元组的存储方式。
⁹ 除非类有一个__slots__
属性,如“使用 slots 节省内存”中所解释的那样。
¹⁰ 这可能很有趣,但并不是非常重要。加速只会在评估集合字面值时发生,而这最多只会发生一次 Python 进程—当模块最初编译时。如果你好奇,可以从dis
模块中导入dis
函数,并使用它来反汇编set
字面值的字节码—例如,dis('{1}')
—和set
调用—dis('set([1])')
。
第四章:Unicode 文本与字节
人类使用文本。计算机使用字节。
Esther Nam 和 Travis Fischer,“Python 中的字符编码和 Unicode”¹
Python 3 引入了人类文本字符串和原始字节序列之间的明显区别。将字节序列隐式转换为 Unicode 文本已经成为过去。本章涉及 Unicode 字符串、二进制序列以及用于在它们之间转换的编码。
根据您在 Python 中的工作类型,您可能认为理解 Unicode 并不重要。这不太可能,但无论如何,无法避免str
与byte
之间的分歧。作为奖励,您会发现专门的二进制序列类型提供了 Python 2 通用str
类型没有的功能。
在本章中,我们将讨论以下主题:
- 字符、代码点和字节表示
- 二进制序列的独特特性:
bytes
、bytearray
和memoryview
- 完整 Unicode 和传统字符集的编码
- 避免和处理编码错误
- 处理文本文件时的最佳实践
- 默认编码陷阱和标准 I/O 问题
- 使用规范化进行安全的 Unicode 文本比较
- 用于规范化、大小写折叠和强制去除变音符号的实用函数
- 使用
locale
和pyuca库正确对 Unicode 文本进行排序 - Unicode 数据库中的字符元数据
- 处理
str
和bytes
的双模式 API
本章新内容
Python 3 中对 Unicode 的支持是全面且稳定的,因此最值得注意的新增内容是“按名称查找字符”,描述了一种用于搜索 Unicode 数据库的实用程序——这是从命令行查找带圈数字和微笑猫的好方法。
值得一提的一项较小更改是关于 Windows 上的 Unicode 支持,自 Python 3.6 以来更好且更简单,我们将在“注意编码默认值”中看到。
让我们从不那么新颖但基础的概念开始,即字符、代码点和字节。
注意
对于第二版,我扩展了关于struct
模块的部分,并在fluentpython.com的伴随网站上发布了在线版本“使用 struct 解析二进制记录”。
在那里,您还会发现“构建多字符表情符号”,描述如何通过组合 Unicode 字符制作国旗、彩虹旗、不同肤色的人以及多样化家庭图标。
字符问题
“字符串”的概念足够简单:字符串是字符序列。问题在于“字符”的定义。
在 2021 年,我们对“字符”的最佳定义是 Unicode 字符。因此,我们从 Python 3 的str
中获取的项目是 Unicode 字符,就像在 Python 2 中的unicode
对象中获取的项目一样——而不是从 Python 2 的str
中获取的原始字节。
Unicode 标准明确将字符的身份与特定字节表示分开:
- 字符的身份——其代码点——是从 0 到 1,114,111(十进制)的数字,在 Unicode 标准中显示为带有“U+”前缀的 4 到 6 位十六进制数字,从 U+0000 到 U+10FFFF。例如,字母 A 的代码点是 U+0041,欧元符号是 U+20AC,音乐符号 G 谱号分配给代码点 U+1D11E。在 Unicode 13.0.0 中,约 13%的有效代码点有字符分配给它们,这是 Python 3.10.0b4 中使用的标准。
- 表示字符的实际字节取决于正在使用的编码。编码是一种将代码点转换为字节序列及其反向转换的算法。字母 A(U+0041)的代码点在 UTF-8 编码中被编码为单个字节
\x41
,在 UTF-16LE 编码中被编码为两个字节\x41\x00
。另一个例子,UTF-8 需要三个字节—\xe2\x82\xac
—来编码欧元符号(U+20AC),但在 UTF-16LE 中,相同的代码点被编码为两个字节:\xac\x20
。
从代码点转换为字节是编码;从字节转换为代码点是解码。参见 示例 4-1。
示例 4-1. 编码和解码
>>> s = 'café' >>> len(s) # ① 4 >>> b = s.encode('utf8') # ② >>> b b'caf\xc3\xa9' # ③ >>> len(b) # ④ 5 >>> b.decode('utf8') # ⑤ 'café'
①
str
'café'
有四个 Unicode 字符。
②
使用 UTF-8 编码将 str
编码为 bytes
。
③
bytes
字面量有一个 b
前缀。
④
bytes
b
有五个字节(“é”的代码点在 UTF-8 中编码为两个字节)。
⑤
使用 UTF-8 编码将 bytes
解码为 str
。
提示
如果你需要一个记忆辅助来帮助区分 .decode()
和 .encode()
,说服自己字节序列可以是晦涩的机器核心转储,而 Unicode str
对象是“人类”文本。因此,将 bytes
解码 为 str
以获取可读文本是有意义的,而将 str
编码 为 bytes
用于存储或传输也是有意义的。
尽管 Python 3 的 str
在很大程度上就是 Python 2 的 unicode
类型换了个新名字,但 Python 3 的 bytes
并不仅仅是旧的 str
更名,还有与之密切相关的 bytearray
类型。因此,在进入编码/解码问题之前,值得看一看二进制序列类型。
字节要点
新的二进制序列类型在许多方面与 Python 2 的 str
不同。首先要知道的是,有两种基本的内置二进制序列类型:Python 3 中引入的不可变 bytes
类型和早在 Python 2.6 中添加的可变 bytearray
。² Python 文档有时使用通用术语“字节字符串”来指代 bytes
和 bytearray
。我避免使用这个令人困惑的术语。
bytes
或 bytearray
中的每个项都是从 0 到 255 的整数,而不是像 Python 2 的 str
中的单个字符字符串。然而,二进制序列的切片始终产生相同类型的二进制序列,包括长度为 1 的切片。参见 示例 4-2。
示例 4-2. 作为 bytes
和 bytearray
的五字节序列
>>> 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')
①
可以从 str
构建 bytes
,并给定一个编码。
②
每个项都是 range(256)
中的整数。
③
bytes
的切片也是 bytes
——即使是单个字节的切片。
④
bytearray
没有字面量语法:它们显示为带有 bytes
字面量作为参数的 bytearray()
。
⑤
bytearray
的切片也是 bytearray
。
警告
my_bytes[0]
检索一个 int
,但 my_bytes[:1]
返回长度为 1 的 bytes
序列,这只是因为我们习惯于 Python 的 str
类型,其中 s[0] == s[:1]
。对于 Python 中的所有其他序列类型,1 项不等于长度为 1 的切片。
尽管二进制序列实际上是整数序列,但它们的字面值表示反映了 ASCII 文本经常嵌入其中的事实。因此,根据每个字节值的不同,使用四种不同的显示方式:
- 对于十进制代码为 32 到 126 的字节——从空格到
~
(波浪号)——使用 ASCII 字符本身。 - 对于制表符、换行符、回车符和
\
对应的字节,使用转义序列\t
、\n
、\r
和\\
。 - 如果字节序列中同时出现字符串定界符
'
和"
,则整个序列由'
定界,并且任何'
都会被转义为\'
。³ - 对于其他字节值,使用十六进制转义序列(例如,
\x00
是空字节)。
这就是为什么在 示例 4-2 中你会看到 b'caf\xc3\xa9'
:前三个字节 b'caf'
在可打印的 ASCII 范围内,而最后两个不在范围内。
bytes
和bytearray
都支持除了依赖于 Unicode 数据的格式化方法(format
,format_map
)和那些依赖于 Unicode 数据的方法(包括casefold
,isdecimal
,isidentifier
,isnumeric
,isprintable
和encode
)之外的所有str
方法。这意味着您可以使用熟悉的字符串方法,如endswith
,replace
,strip
,translate
,upper
等,与二进制序列一起使用——只使用bytes
而不是str
参数。此外,如果正则表达式是从二进制序列而不是str
编译而成,则re
模块中的正则表达式函数也适用于二进制序列。自 Python 3.5 以来,%
运算符再次适用于二进制序列。⁴
二进制序列有一个str
没有的类方法,称为fromhex
,它通过解析以空格分隔的十六进制数字对构建二进制序列:
>>> bytes.fromhex('31 4B CE A9') b'1K\xce\xa9'
构建bytes
或bytearray
实例的其他方法是使用它们的构造函数,并提供:
- 一个
str
和一个encoding
关键字参数 - 一个可提供值从 0 到 255 的项目的可迭代对象
- 一个实现缓冲区协议的对象(例如,
bytes
,bytearray
,memoryview
,array.array
),它将源对象的字节复制到新创建的二进制序列中
警告
直到 Python 3.5,还可以使用单个整数调用bytes
或bytearray
来创建一个以空字节初始化的该大小的二进制序列。这个签名在 Python 3.5 中被弃用,并在 Python 3.6 中被移除。请参阅PEP 467—二进制序列的次要 API 改进。
从类似缓冲区的对象构建二进制序列是一个涉及类型转换的低级操作。在示例 4-3 中看到演示。
示例 4-3。从数组的原始数据初始化字节
>>> 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' # ③
①
类型码'h'
创建一个短整数(16 位)的array
。
②
octets
保存构成numbers
的字节的副本。
③
这是代表 5 个短整数的 10 个字节。
从任何类似缓冲区的源创建bytes
或bytearray
对象将始终复制字节。相反,memoryview
对象允许您在二进制数据结构之间共享内存,正如我们在“内存视图”中看到的那样。
在这对 Python 中二进制序列类型的基本探索之后,让我们看看它们如何转换为/从字符串。
基本编码器/解码器
Python 发行版捆绑了 100 多个编解码器(编码器/解码器),用于文本到字节的转换以及反之。每个编解码器都有一个名称,如'utf_8'
,通常还有别名,如'utf8'
,'utf-8'
和'U8'
,您可以将其用作函数中的encoding
参数,如open()
,str.encode()
,bytes.decode()
等。示例 4-4 展示了相同文本编码为三种不同的字节序列。
示例 4-4。使用三种编解码器对字符串“El Niño”进行编码,生成非常不同的字节序列
>>> 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'
流畅的 Python 第二版(GPT 重译)(二)(3)https://developer.aliyun.com/article/1484411