一、字典新用法
1.1 字典推导式
自 Python 2.7 开始,列表推导式和生成器表达式经过改造,以适用于字典推导式(以及后文要讲的集合推导式)。字典推导式从任何可迭代对象中获取键值对,构建 dict 实例。
# 这种包含键值对的可迭代对象可以直接传给 dict 构造函数
dial_codes = [(880, 'Bangladesh'), (55, 'Brazil'), (86, 'China'), (91, 'India'), (62, 'Indonesia'), (81, 'Japan'),
(234, 'Nigeria'), (92, 'Pakistan'), (7, 'Russia'), (1, 'United States')]
# 字典推导式;这里 我们对调了键和值的位置,以 country 为键,以 code 为值
country_dial = {country: code for code, country in dial_codes}
print(
country_dial) # {'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62, 'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
code_up = {
code: country.upper() for country, code in sorted(country_dial.items())
}
print(code_up) # {880: 'BANGLADESH', 55: 'BRAZIL', 86: 'CHINA', 91: 'INDIA', 62: 'INDONESIA', 81: 'JAPAN', 234: 'NIGERIA', 92: 'PAKISTAN', 7: 'RUSSIA', 1: 'UNITED STATES'}
1.2 映射拆包
首先,调用函数时,不止一个参数可以使用**
。但是,所有键都要是字符串,而且在所有参数中是唯一的(因为关键字参数不可重复)。
还记得列表推导式吗,拆包时使用过的是
*
来接收;这里字典使用的是**
哦
# 参数是一个字典类型的拆包,**说明参数可以是多个
def dump(**kwargs):
return kwargs
# 传参数的时候,需要**配合字典来传递
ret = dump(**{'x': 1}, y=2, **{'z': 3})
print(ret) # {'x': 1, 'y': 2, 'z': 3}
其次,**
可在 dict 字面量
中使用,同样可以多次使用。
print({'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}})
# {'a': 0, 'x': 4, 'y': 2, 'z': 3}
这种情况下允许键重复,后面的键覆盖前面的键,比如本例中 x 映射的值。
1.3 使用 | 合并映射
Python 3.9 支持使用 |
和 |=
合并映射。这不难理解,因为二者也是并集运算符。**|**
运算符创建一个新映射。
d1 = {'a': 1, 'b': 3}
d2 = {'a': 2, 'b': 4, 'c': 6}
print(d1) # {'a': 1, 'b': 3}
print(d2) # {'a': 2, 'b': 4, 'c': 6}
# 结果具有并集操作
print(d1 | d2) # {'a': 2, 'b': 4, 'c': 6}
通常,新映射的类型与左操作数(本例中的 d1)的类型相同。不过,涉及用户定义的类型时,也可能与第二个操作数的类型相同。
如果想就地更新现有映射,则使用|=
。续前例,当时 d1 没有变化,但是现在变了。
d1 = {'a': 1, 'b': 3}
d2 = {'a': 2, 'b': 4, 'c': 6}
print(d1) # {'a': 1, 'b': 3}
print(d2) # {'a': 2, 'b': 4, 'c': 6}
# 结果具有并集操作
print(d1 |= d2)
二、使用模式匹配处理映射
match/case
语句的匹配对象可以是映射。映射的模式看似 dict 字面量,其实能够匹配collections.abc.Mapping
的任何具体子类或虚拟子类
例子:get_creators
函数有一些简单的类型注解,作用是明确表明参数为一个 dict
,返回值是一个 list
。
def get_creators(record: dict) -> list:
match record:
# 匹配含有 'type': 'book', 'api' :2,而且 'authors' 键映射一个序列的映射对象。以列表形式返回序列中的项。
case {'type': 'book', 'api': 2, 'authors': [*names]}:
return names
# 匹配含有 'type': 'book', 'api' :1,而且 'authors' 键映射任何对象的映射对象。以列表形式返回匹配的对象
case {'type': 'book', 'api': 1, 'author': name}:
return [name]
# 其他含有 'type': 'book' 的映射均无效,抛出 ValueError
case {'type': 'book'}:
raise ValueError(f"Invalid 'book' record: {record!r}")
# 匹配含有 'type': 'movie',而且 'director' 映射单个对象的映射对象。以列表形式返回匹配的对象。
case {'type': 'movie', 'director': name}:
return [name]
# 其他匹配对象均无效,抛出 ValueError。
case _:
raise ValueError(f'Invalid record: {record!r}')
# 定义一个字典
b1 = dict(api=1, author='Douglas Hofstadter', type='book', title='Gödel, Escher, Bach')
print(get_creators(b1)) # ['Douglas Hofstadter']
# from语句写在这里,目的是为了演示
from collections import OrderedDict
b2 = OrderedDict(api=2, type='book', title='Python in a Nutshell', authors='Martelli Ravenscroft Holden'.split())
print(get_creators(b2)) # ['Martelli', 'Ravenscroft', 'Holden']
print(get_creators({'type': 'book', 'pages': 770})) # ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
注意,模式中键的顺序无关紧要。即使 b2 是一个 OrderedDict,也能作为匹配对象。
与序列模式不同,就算只有部分匹配,映射模式也算成功匹配。在上述代码中,b1 和 b2 两个匹配对象中都有 ‘title’ 键,尽管任何 ‘book’ 模式中都没有这个键,但依然可以匹配。
没有必要使用 **extra
匹配多出的键值对,倘若你想把多出的键值对捕获到一个 dict 中,可以在一个变量前面加上 **
,不过必须放在模式最后。**_ 是无效的,纯属画蛇添足。下面是一个简单的例子。
food = dict(category='ice cream', flavor='vanilla', cost=199)
match food:
case {'category': 'ice cream', **details}:
print(f'Ice cream details: {details}') # Ice cream details: {'flavor': 'vanilla', 'cost': 199}
三、映射类型的标准 API
collections.abc
模块中的抽象基类 Mapping
和 MutableMapping
描述 dict
和类似类型的接口,如图
这两个抽象基类的主要作用是确立映射对象的标准接口,并在需要广义上的映射对象时为 isinstance 提供测试标准。
my_dict = {}
print(isinstance(my_dict, abc.Mapping)) # True
print(isinstance(my_dict, abc.MutableMapping)) # True
如果想自定义映射类型,扩展collections.UserDict
或通过组合模式包装 dict 更简单,那就不要定义这些抽象基类的子类。collections.UserDict 类和标准库中的所有具体映射类都在实现中封装了基本的 dict,而 dict 又建立在哈希表之上。因此,这些类有一个共同的限制,即键必须可哈希(值不要求可哈希,只有键需要)
3.1 `可哈希`指什么
如果一个对象的哈希码在整个生命周期内永不改变(依托
__hash__()
方法),而且可与其他对象比较(依托__eq__()
方法),那么这个对象就是可哈希的。两个可哈希对象仅当哈希码相同时相等。
-
数值类型以及不可变的扁平类型
str
和bytes
均是可哈希的。 -
如果容器类型是不可变的,而且所含的对象全是可哈希的,那么容器类型自身也是可哈希的。
-
frozenset 对象全部是可哈希的,因为按照定义,每一个元素都必须是可哈希的。
-
仅当所有项均可哈希,tuple 对象才是可哈希的。
# 因为元组中的每个元素,都是不可变的,所以是可hash的
tt = (1, 2, (30, 40))
print(hash(tt)) # -3907003130834322577
# 而这个元组中的有一个可变的列表元素,此时hash就会报错
tl = (1, 2, [30, 40])
print(hash(tl)) # TypeError: unhashable type: 'list'
# frozenset把列表变成了不可变的,那就是可hash的
tf = (1, 2, frozenset([30, 40]))
print(hash(tf)) # 5149391500123939311
默认情况下,用户定义的类型是可哈希的,因为自定义类型的哈希码取自id()
,而且继承自object
类的__eq__()
方法只不过是比较对象ID
。如果自己实现了__eq__()
方法,根据对象的内部状态进行比较,那么仅当__hash__()
方法始终返回同一个哈希码时,对象才是可哈希的。实践中,这要求 __eq__()
和__hash__()
只考虑在对象的生命周期内始终不变的实例属性。
3.2 常用映射方法概述
下图列出了dict
,以及collections
模块中两个常用变体defaultdict
和OrderedDict
实现的方法。简单起见,省略了 object 实现的方法,[...]
表示可选参数
3.3 插入或更新可变的值
根据 Python 的“快速失败”原则,当键 k
不存在时,d[k]
抛出错误。深谙 Python 的人知道,如果觉得默认值比抛出KeyError
更好,那么可以把d[k]
换成d.get(k, default)
。然而,如果你想更新得到的可变值,那么还有更好的方法。
3.4 自动处理缺失的键
有时搜索的键不一定存在,为了以防万一,可以人为设置一个值,以方便某些情况的处理。人为设置的值主要有两种方法:
-
第一种是把普通的 dict 换成 defaultdict;
-
第二种是定义 dict 或其他映射类型的子类,实现
missing
方法。下面分别介绍这两种情况。
defaultdict:处理缺失键的另一种选择
对于 collections.defaultdict,d[k] 句法找不到搜索的键时,使用指定的默认值创建对应的项。
`missing` 方法
映射处理缺失键的底层逻辑在__missing__
方法中。dict 基类本身没有定义这个方法,但是如果dict的子类定义了这个方法,那么dict.__getitem__
找不到键时将调用__missing__
方法,不抛出KeyError。
四、dict 的变体
4.1 collections.OrderedDict
自 Python 3.6 起,内置的 dict 也保留键的顺序。使用 OrderedDict 最主要的原因是编写与早期 Python版本兼容的代码。不过,dict 和 OrderedDict 之间还有一些差异,Python 文档中有说明,摘录如下 (根据日常使用频率,顺序有调整)。
-
OrderedDict 的等值检查考虑顺序。
-
OrderedDict 的
popitem()
方法签名不同,可通过一个可选参数指定移除哪一项。 -
OrderedDict 多了一个
move_to_end()
方法,便于把元素的位置移到某一端。 -
常规的 dict 主要用于执行映射操作,插入顺序是次要的。
-
OrderedDict 的目的是方便执行重新排序操作,空间利用率、迭代速度和更新操作的性能是次要的。
-
从算法上看,OrderedDict 处理频繁重新排序操作的效果比 dict 好,因此适合用于跟踪近期存取情况(例如在 LRU 缓存中)。
4.2 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)
print(chain['a']) # 1
print(chain['c']) # 6
ChainMap 实例不复制输入映射,而是存放映射的引用。ChainMap 的更新或插入操作只影响第一个输入映射。续前例:
d1 = dict(a=1, b=3)
d2 = dict(a=2, b=4, c=6)
from collections import ChainMap
chain = ChainMap(d1, d2)
print(chain['a']) # 1
print(chain['c']) # 6
chain['c'] = -1
print(d1) # {'a': 1, 'b': 3, 'c': -1}
print(d2) # {'a': 2, 'b': 4, 'c': 6}
4.2 collections.Counter
这是一种对键计数的映射。更新现有的键,计数随之增加。可用于统计可哈希对象的实例数量,或者作为多重集(multiset)使用。Counter 实现了组合计数的+
和-
运算符,以及其他一些有用的方法,例如most_common([n])
。该方法返回一个有序元组列表,对应前 n 个计数值最大的项及其数
量。下面使用 Counter 统计词中的字母数量。
import collections
ct = collections.Counter('abracadabra')
print(ct) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
ct.update('aaaaazzz')
print(ct) # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(ct.most_common(3)) # [('a', 10), ('z', 3), ('b', 2)]
注意,’b’ 和 ‘r’ 两个键并列第三,但是 ct.most_common(3) 只显示 3 项。
若想把 collections.Counter 当作多重集使用,假设各个键是集合中的元素,计数值则是元素在集合中出现的次数。
4.4 shelve.Shelf
标准库中的 shelve 模块持久存储字符串键与(以 pickle 二进制格式序列化的)Python 对象之间的映射。
模块级函数shelve.open
返回一个shelve.Shelf
实例,这是一个简单的键值 DBM 数据库,背后是dbm 模块。shelve.Shelf 具有以下特征。
-
shelve.Shelf 是 abc.MutableMapping 的子类,提供了我们预期的映射类型基本方法。
-
此外,shelve.Shelf 还提供了一些其他 I/O 管理方法,例如 sync 和 close。
-
Shelf 实例是上下文管理器,因此可以使用 with 块确保在使用后关闭。
-
为键分配新值后即保存键和值。
-
键必须是字符串。
-
值必须是 pickle 模块可以序列化的对象。
4.5 子类应继承 UserDict 而不是 dict
创建新的映射类型,最好扩展collections.UserDict
,而不是dict
。
子类最好继承 UserDict 的主要原因是,内置的 dict 在实现上走了一些捷径,如果继承 dict,那就不得不覆盖一些方法,而继承 UserDict 则没有这些问题。
注意,UserDict 没有继承 dict,使用的是组合模式:内部有一个 dict 实例,名为 data,存放具体的项。这样做可以避免 __setitem__
等特殊方法意外递归,还能简化 __contains__
的实现。
# StrKeyDict 扩展 UserDict。
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
# __setitem__ 把 key 转换成 str 类型。委托给 self.data 属性之后,这个方法更易于重写。
def __setitem__(self, key, item):
self.data[str(key)] = item
五、不可变映射
标准库提供的映射类型都是可变的,不过有时也需要防止用户意外更改映射。
types 模块提供的 MappingProxyType 是一个包装类,把传入的映射包装成一个 mappingproxy 实例,这是原映射的动态代理,只可读取。这意味着,对原映射的更新将体现在 mappingproxy 实例身上, 但是不能通过 mappingproxy 实例更改映射。
from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
# d_proxy 可以访问 d 中的项。
print(d_proxy) # {1: 'A'}
print(d_proxy[1]) # A
# 不能通过 d_proxy 更改映射
d_proxy[2] = 'x' # TypeError: 'mappingproxy' object does not support item assignment
# d_proxy 是动态的,能够反映 d 的变化。
d[2] = "B"
print(d_proxy[2]) # B
六、字典视图
dict 的实例方法.keys()
、.values()
和.items()
分别返回dict_keys
、dict_values
和 dict_items
类的实例。这些字典视图是 dict 内部实现使用的数据结构的只读投影。Python 2 中对应的方法返回列表,重复 dict 中已有的数据,有一定的内存开销。另外,视图还取代了返回迭代器的旧方法。
d = dict(a=10, b=20, c=30)
values = d.values()
# 通过视图对象的字符串表示形式查看视图的内容。
print(values) # dict_values([10, 20, 30])
# 可以查询视图的长度。
print(len(values)) # 3
# 视图是可迭代对象,方便构建列表
print(list(values)) # [10, 20, 30]
# 视图实现了 __reversed__ 方法,返回一个自定义迭代器
print(reversed(values)) # <dict_reversevalueiterator object at 0x00000166540199E0>
# 不能使用 [] 获取视图中的项
print(values[0]) # TypeError: 'dict_values' object is not subscriptable
视图对象是动态代理。更新原 dict 对象后,现有视图立即就能看到变化。
d['z'] = 99
print(d) # {'a': 10, 'b': 20, 'c': 30, 'z': 99
print(values) # dict_values([10, 20, 30, 99])
dict_values 类是最简单的字典视图,只实现了__len__
、__iter__
和 __reversed__
这 3 个特殊方法。除此之外,dict_keys 和 dict_items 还实现了多个集合方法,基本与 frozenset 类相当。
七、集合
集合,一词指代 set 和 frozenset(不可变set类型)。
集合是一组唯一的对象。集合的基本作用是去除重复项。
# 定义一个列表,这个列表中数据有重复的
list = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
# 把列表转换成集合,就能去重复
print(set(list)) # {'bacon', 'spam', 'eggs'}
如果想去除重复项,同时保留每一项首次出现位置的顺序,那么现在使用普通的 dict 即可, 如下所示。
# 定义一个列表,这个列表中数据有重复的
list = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
# 去重重复,且只保留第一次出现位置的顺序,利用了dict特性
print(dict.fromkeys(list).keys()) # dict_keys(['spam', 'eggs', 'bacon'])
集合元素必须是可哈希的对象。set 类型不可哈希,因此不能构建嵌套 set 实例的 set 对象。但是 frozenset 可以哈希,所以 set 对象可以包含 frozenset 元素。
除了强制唯一性之外,集合类型通过中缀运算符实现了许多集合运算。给定两个集合 a 和 b,a | b
计算 并集,a & b
计算交集,a - b
计算差集,a ^ b
计算对称差集。
# set1集合内容多
set1 = {"hello", "world", "china", "beijing", "zhonguo", "henan", "shandong"}
# set2集合少
set2 = {"china", "beijing", "shandong", "nanjing"}
# 统计 set1中的也在set2中出现过的,用交集
print(set1 & set2) # {'shandong', 'china', 'beijing'}
# 统计,两个集合中所有的,用并集
print(set1 | set2) # {'henan', 'nanjing', 'shandong', 'china', 'beijing', 'zhonguo', 'hello', 'world'}
# 统计,set1中有,set2没有的,用差集
print(set1 - set2) # {'hello', 'world', 'henan', 'zhonguo'}
# 统计set2有,set1没有的,用差集
print(set2 - set1) # {'nanjing'}
# 对称差集 被称为异或集,是集合论中的一个概念,表示两个集合中不共有的元素构成的集合
print(set1 ^ set2) # {'world', 'nanjing', 'hello', 'zhonguo', 'henan'}
print(set2 ^ set1) # {'world', 'nanjing', 'hello', 'henan', 'zhonguo'}
注意:上述例子中,直接使用的set集合,当然也可以是list,然后转换成set后计算。
10.1 set 字面量
set 字面量的句法与集合的数学表示法几乎一样,例如 {1}
、{1, 2}
等。唯有一点例外:空 set 没有字面量表示法
,必须写作 set()
。
注意点
创建空 set,务必使用不带参数的构造函数,即set()
。倘若写成{}
,则创建的是空dict
。
在 Python 3 中,集合的标准字符串表示形式始终使用 {...}
表示法,唯有空集例外。
s = {1}
print(type(s)) # <class 'set'>
print(s) # {1}
print(s.pop()) # 1
print(s) # set()
与调用构造函数(例如 `set([1, 2, 3])`)相比,使用`set`的字面量句法(例如 `{1, 2, 3}`)不仅速度 快,而且更具可读性。调用构造函数速度慢的原因是,Python 要查找 set 名称,找出构造函数,然后构建 一个列表,最后再把列表传给构造函数。相比之下,Python 处理字面量只需要运行一个专门的 `BUILD_SET` 字节码。
`frozenset` 没有字面量句法,必须调用构造函数创建。在 Python 3 中,`frozenset 的字符串表示形式类似于构造函数调用`。代码如下。
print(frozenset(range(5))) # frozenset({0, 1, 2, 3, 4})
10.2 集合推导式
ss = {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),
'')}
# {'#', '$', '§', '=', '¤', '¶', '÷', 'µ', '©', '¢', '®', '£', '%', '+', '>', '±', '×', '<', '°', '¥', '¬'}
print(ss)
八、集合的实现方式对实践的影响
set 和 frozenset 类型都使用哈希表实现。这种设计带来了以下影响。
-
集合元素必须是可哈希对象,必须正确实现
hash__` 和` __eq
方法。 -
成员测试效率非常高。对于一个包含数百万个元素的集合,计算元素的哈希码就可以直接定位元素, 找出元素的索引偏移量。稍微搜索几次就能找到匹配的元素,即使穷尽搜索开销也不大。
-
与存放元素指针的低层数组相比,集合占用大量内存。尽管集合的结构更紧凑,但是一旦要搜索的元 素数量变多,搜索速度将显著下降。
-
元素的顺序取决于插入顺序,但是顺序对集合没有什么意义,也得不到保障。如果两个元素具有相同 的哈希码,则顺序取决于哪个元素先被添加到集合中。
-
向集合中添加元素后,现有元素的顺序可能发生变化。这是因为哈希表使用率超过三分之二后,算法 效率会有所下降,Python 可能需要移动和调整哈希表的大小,然后重新插入元素,导致元素的相对顺 序发生变化。
8.1 集合运算
返回布尔类型的运算
九、 字典视图的集合运算
.keys()
和 .items()
这两个 dict 方法返回的视图对象与frozenset
极为相似
需要特别注意的是,dict_keys
和 dict_items
实现了一些特殊方法,支持强大的集合运算符,包括 &
(交集)、|
(并集)、-
(差集)和 ^
(对称差集)。
仅当 dict 中的所有值均可哈希时,dict_items 视图才可当作集合使用。倘若 dict 中有 不可哈希的值,对 dict_items 视图做集合运算将抛出 TypeError: unhashable type 'T'
,其 中 T 是不可哈希的类型。相反,dict_keys 视图始终可当作集合使用,因为按照其设计,所有键均可哈希。
原文始发于微信公众号(Python之家):Python深入-3-字典与集合
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198418.html