字典是 Python 中的一种重要的数据类型,它不仅可以用于存储任何映射类型的数据,也是构成 Python 语言的基石,从可变的关键字参数到对象的属性,各个方面都有字典的存在。Python 的字典在保持很高效率的同时,也兼具易用性。
字典类型
Python中的映射
在 Python 中,映射(mapping)是容器类型的一种,映射类型可以存储键(key)值(value)对形式的数据,并且可以根据键快速查找对应的值。collections.abc
模块提供了抽象基类 Mapping
和 MutableMapping
,分别表示映射和可变映射,下图展示了映射类型支持的方法:
字典(dictionary)是 Python 最主要、也是最基本的映射类型。Python 内置类型 dict
提供了对字典的支持。Python 的字典类似其它编程语言的 Map
或 HashMap
,底层原理也是散列表。因此,字典的键必须是一个可散列对象,这和集合是一样的。
注意:由于字典被设计为处理更通用的工作,因此 Python 中的某些映射对象的类型可能不是字典,而是另一种针对性优化后的映射类型:
>>> class Something: ... >>> isinstance(Something.__dict__, dict) False >>> from collections.abc import Mapping >>> isinstance(Something.__dict__, Mapping) True
创建字典
在 Python 中,字典字面量是使用花括号 { }
包含的数据,其中每对键和值对用冒号 :
分割,不同键值对之间用逗号 ,
分割,例如:
在实际存储时,键保证唯一,就像集合一样。但在构造字典时,键可以重复,此时最后的一个键值对会替换前面的键值对。值可以取任何数据类型,但键必须是不可变的数据(更严谨的说是可散列类型),如数值和字符串。
dict()
的构造方法提供了更丰富的构造字典的方式。根据映射的概念,任何由有序的两个元素构成的序列都可以用于构造字典,例如嵌套列表或元组列表:
或者每次产出两个值的迭代器也可以:
还可以通过构造函数中的关键字参数:
当然,还可以通过字典字面量构造另一个字典:
如果只知道字典中包含了哪些键,那么 dict
也提供了一个工厂方法 .fromkeys()
从可迭代对象中构造字典,并将所有的值预先设置为 None
:
还可以通过 .fromkeys()
的第二个参数 initial
设置其它的默认值。
字典的操作
Python 为字典提供了丰富的操作和易用的接口。
元素操作
像序列的索引一样,字典也可以使用方括号根据键获取对应的值:
但是,方括号形式获取值时,如果对应的键不存在,会引发 KeyError
异常。in
操作符可以作用于字典,判断字典中是否有某个键:
一种更为安全的获取字典值的方式是使用 .get()
方法,它会在对应的键不存在时返回 None
,而不是直接发生错误:
但是,使用 .get()
方法不能区分是因为没有对应的键得到 None
,还是这个键映射的值就是 None
。如果确实需要对这两种情况做出区分,应该使用 in
操作符。
.get()
方法的第二个参数 default
指定当键不存在时,方法返回的默认值。
可以使用方括号赋值的形式为字典添加上新的键值对:
以下是一个经典的用法,使用字典统计一系列词汇的词频,.get()
方法的默认值用于处理当前词频中还没有该词汇的情况:
使用 del dict[key]
语句可以直接删除字典键为 key
的键值对,但是它也有对应的键不存在时会引发 KeyError
异常的情况。字典的 .pop(key, default)
可以通过第二个参数 default
处理键不存在时的默认返回值。(但是不像 .get()
方法,在没有使用 default
参数时它还是会发生 KeyError
)
.pop()
方法在删除键值对后可以返回对应的值。以下是一种常见的使用场景,在扩展了某个函数的功能后,使用 .pop()
方法从可变的关键字参数中取出这一环节要用到的值,再将剩下的值转交下一环节统一处理,从而避免多余的参数污染,也保证了类型安全:
最后,.popitem()
方法随机返回一个键值对,并从字典内删除它。
注意,新版本的 Python 中,
.popitem()
方法返回的是最后添加的键值对。稍后会讨论字典的顺序问题。
迭代
字典实现了 .__len__()
方法,因此可以使用 len()
函数获取字典中实际的键值对个数。
默认情况下,对字典的迭代只会获取字典所有键构成的迭代器,如果确实需要同时使用键和值,需要使用 .items()
方法得到所有的键值对:
如果只关心字典的值,可以使用 .values()
方法返回字典所有值。相应地也有 .keys()
方法获取所有键。不过注意:这三个方法返回的结果都不是列表,甚至都不是序列对象,而是一些特殊的字典视图对象,这意味着无法对它们应用列表的方法,甚至都不支持使用索引,也不能直接修改元素。
合并与清空
如果要将一个字典的键值对合并到另一个字典上,可以调用另一个字典的 .update(other)
方法,相同的键由 other
字典决定值。
该方法会改变字典,因此这个方法被命名为“更新”。如果只是想通过合并创建一个新字典,Python3.5 引入了通过两个星号 **
解包形式的字面量语法,从多个字典中创建新字典:
Python3.9 还通过重载按位或运算符 |
实现了更简便的合并方式:
并且通过按位或的赋值形式 |=
可以实现更简洁的更新操作。
在 Python3.4 及之前的版本如果要通过合并创建新字典,可能需要用到字典的 .copy()
方法复制自身,并且做的也是浅复制。
最后,.clear()
方法用于清空字典,移除字典中的所有键值对。
不可变字典
在 Python 中,列表有对应不可变的类型元素,集合也有对应不可变的类型
frozenset
,但是字典却没有对应不可变的类型。这是由于不可变字典的应用场景实在很少,而且多数可能的应用场景都可以被命名元组或frozenset(dict.items())
取代。如果仅仅是想要阻止使用者应用修改操作,可以使用
types
模块提供的MappingProxyType
类,这个类在使用者尝试应用修改相关的操作时会抛出异常:>>> from types import MappingProxyType >>> immutable_dict = MappingProxyType({'a': 1, 'b': 2, 'c': 3}) >>> immutable_dict['b'] 2 >>> immutable_dict['b'] = 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'mappingproxy' object does not support item assignment但是,
MappingProxyType
存在一些问题:
MappingProxyType
是一个代理(proxy)对象,即它只保存了对原始映射的引用,绕过MappingProxyType
直接修改原始映射依旧是可行的- 因此,
MappingProxyType
仍然是不可散列的类型,不能作为集合的元素或字典的键如果确实需要一个完善的不可变的字典,可以检索 pypi 上的第三方库。
字典推导式
列表推导式和生成器表达式的概念也可以应用到字典上。字典推导式(dict comprehension)可以从任何可迭代对象中提起键值对,从而构造字典。
例如,对于以下数据:
字典推导式的用法示例为:
字典的处理
字典的默认值
字典处理起来有一个常见的问题:在通过键获取值时,这个键可能不存在字典中,此时就需要考虑如何处理缺失值。
显然,最差的处理方式就是通过捕获 KeyError
异常,在 except
中处理键不在字典中的情况。一般情况下,.get()
方法可以代替 in
操作符的功能,同时处理键存在与不存在的两种值的情况,就像以上的示例一样。
但是,如果处理的值比较复杂,.get()
方法用起来可能也不太方便。例如,如果要扩充以上统计词频的功能,使其能保存每次出现的位置,那么就无法在一行内处理了:
如果不是执意要用 .get()
方法,使用 in
操作符提前检查对应的键是否存在,可能稍微易读一些:
实际上,Python 的字典还提供了一个 .setdefault(key, default)
方法,它的处理方式为:如果字典有键 key
,那么返回它对应的值;如果没有这个键,为字典添加上 key
键和它对应的值 default
后,再返回设置的值 default
。虽然它的名字带上了 set ,但它实际上是一种会在键缺失时做 set 的 get 行为。
.setdefault(key, default)
方法的这种行为就非常适合用于处理键缺失的情况。例如,以上情况使用 .setdefault()
就可以在一行内处理完了:
不过,setdefault
这个名字可能会给没有见过该方法的人带来一定困惑。实际应该使用哪种处理方式,可以根据使用场景决定。
这几种处理方式虽然一定程度上解决了缺失值的问题,但还存在一个关键的问题:这些缺失值是字典的使用者提供的,而不是这个字典的创建者提供的:如果这个字典是某个第三方库提供的,可能很难理解它期待使用什么值作为默认值;并且如果这个默认值构造起来比较复杂,在程序中多次处理也比较麻烦。
为了解决这个问题,可以使用 collections
提供的 defaultdict
。defaultdict
是 dict
的子类,用于在尝试获取不存在的键的值时,使用预先设置的函数提供默认值。其构造函数为:
可以为可选参数 default_factory
提供一个工厂函数,为字典中不存在的键构造初始值。其它参数同 dict
的构造函数。
例如,以上词频统计的示例要求在词语不存在时使用空列表作为默认值,那么就可以为 default_factory
提供列表的构造函数:
这样使用者在任何时候都可以直接访问 occurrence
的任何键,缺失的情况将由字典本身处理:
这样的代码写起来就非常简洁且清晰了。
defaultdict
可以自动处理键不存在的情况,是因为它实现了 __missing__
特殊方法,它是针对映射类型的特殊方法,因为映射类型经常需要处理键不存在(即 .__getitem__()
失败)的情况,此时 .__missing__()
就用于处理这种情况。dict
并没有实现这种方法,所以它只会抛出异常。
defaultdict
是一种实现了 .__missing__()
的字典,它会将工厂函数存储到 .default_factory
属性,在 .__missing__()
中通过这个属性给未找到的元素设置值。注意:由于 .__missing__()
不会被 .__getitem__()
以外的其它方法调用,因此 defaultdict
的 .get()
方法会像正常的 dict
那样返回 None
,而不是使用 default_factory
提供的默认值。
defaultdict
的 default_factory
要求是一个无参数函数,这就意味着它不能根据不同键的情况设置不同的默认值。如果确实有这个要求,可以通过继承 dict
并提供自定义的 .__missing__(self, key)
方法处理键 key
不同的默认值处理方式。
例如,以下实现了一个 FilePool
类,它会在键不存在时创建新的文件名与文件 IO 类,自动管理文件对象的打开与关闭:
这个类在键不存在时会自动处理文件的打开工作,并在变量回收前处理文件的关闭工作:
字典的顺序
字典并不是序列,字典的键值对之间并不总是按添加的顺序排列,因此不要依赖字典的遍历顺序来做任何假设或操作:
例如,假设要从某个排序后的序列构建字典,再将这个字典转换为序列就可能破坏这个顺序。如果有顺序的要求,collections
提供了 OrderedDict
类,它是一种有序字典,可以严格地记录字典中各键值对的顺序。
OrderedDict
的构造函数与 dict
构造函数的用法一致,其余方法用法也完全相同,只有 .popitem()
结果有一些不同:dict.popitem()
返回的键值对可能是随机的,但 OrderedDict.popitem()
会移除并返回字典里最后插入的键值对;并且该方法还有一个额外的 last=True
参数,如果传入 False
会使其移除并返回最先插入的键值对。
OrderedDict
还提供了 .move_to_end(key, last=True)
方法,可以移动键 key
到最后。如果 last
值为 False
,则移动到最前面:
现在的 Python 已经很少见到 OrderedDict
的使用了,这是因为在 Python3.6 后,内置的字典已经可以保留键值对在添加时的顺序了;Python3.7 正式将字典的有序性作为语言的规范。但是直到 Python3.8 字典才实现了 .__reversed__()
方法,使其可以被逆序迭代。考虑到兼容性,最好还是采用 OrderedDict
处理有序的映射。
虽然现在 dict
和 OrderedDict
用起来几乎完全一致,但是由于底层实现不太一样,两者用起来仍然存在一定区别:
- 标准的
dict
是专门用于处理映射的类型,它在处理映射信息时速度很快,且占用空间也小 OrderedDict
更擅长处理键值对的顺序,可以比dict
更好地处理频繁的重新排序操作,这使其更适用于跟踪最近的访问。但OrderedDict
的空间效率和更新操作的性能则稍微差一些
OrderedDict
的一个典型应用就是实现 LRU(Least Recently Used) 缓存,这种缓存会在容量达到限制之后,首先移除最久没有用到的缓存项。使用有序的字典可以很容易地实现这种缓存。
字典的排序
对于有序的字典,将其排序也是一种常见的需求。但是注意:内置的 sorted()
函数在处理字典时,只会迭代字典的所有键,所以为了保留映射关系,需要使用 items()
获取键值对的可迭代对象:
因为 sorted()
函数会以元组的第一个参数作为排序依据,如果需要对字典值做排序,需要使用 key
参数改变排序的依据:
这样这个函数就可以处理字典值的排序要求了,使用效果为:
可以使用该函数对上文处理的词频字典排序。如果需要处理这种计数问题,可以使用 collections
提供的 Counter
类,它是 dict
的子类,专门用于计数可散列对象。它会将被计数的元素存储为字典的键,计数结果存储为值。计数结果是一个整型数值,包括 0 和负数。
Counter
的构造函数用法和字典很像,但它可以接收一个序列或序列型可迭代对象,表示对该序列对象的元素计数:
如果直接传入一个映射,表示直接使用这个映射作为计数结果。但 Counter
并没有提供 .fromkeys()
方法,不能直接用序列生成一系列键。
通常字典方法都可用于 Counter
对象,但在查询时如果键不存在,Counter
不会报错而是返回 0 :
Counter
的 .update()
也可以更新自身,不过参数可以是任意它的构造方法能接受的值,从而方便继续计数:
Counter
还提供了一些额外的方法处理其它计数问题。例如,.subtract()
方法从参数中减去计数结果,相当于 .update()
的反向操作。但是在相减后,计数结果可能出现零或负数:
除此之外,Counter
还通过重载运算符实现了多个计数结果的组合:
使用运算符的合并会自动过滤为零或负值的计数结果。
如果要对计数结果做进一步统计,Counter
也提供了相应的方法。例如,.most_common(n=None)
会返回计数值最大的 n
个元素。如果忽略 n
,则依照计数值从大到小返回所有元素:
.elements()
用于从计数结果恢复原有序列:
Python3.10 还为 Counter
引入了一个新的方法 .total()
,用于计算计数总和:
使用ChainMap连接字典
Python 3.3 在标准库 collections
引入了一个新的容器 ChainMap
,它提供了一种将多个映射链式相接实现合并效果的方式。
ChainMap
的构造函数可以接受任意个映射对象,它将这些映射对象“串联”成一个统一的、可读写的视图。相比 dict.update
,ChainMap
的连接不涉及拷贝,因此效率很高。
ChainMap
会将这些映射对象收集在一个列表中,可以通过 .maps
属性获取这个映射列表。ChainMap
支持所有字典的操作方法,具体实现的细节为:
- 查找时依次搜索各映射,找到一个存在的键便返回;
- 写入、更新、删除操作只作用于第一个映射。
由于 ChainMap
只保存映射的引用,因此如果通过别的途径修改映射对象,ChainMap
也能实时更新。
ChainMap
可以用于具有嵌套关系的查找上下文,这样各个上下文的变化都能实时更新,也不会互相影响。Python 官方文档列举了两个 ChainMap
的典型应用场景:
ChainMap
可以很方便地实现嵌套作用域(如locals > globals > builtins
),例如 Python 内部作用域的查找可以表示为:
ChainMap
还用于配置覆盖(如命令行参数 > 环境变量 > 默认值),例如:
除了字典通用的操作方法外,ChainMap
还引入了以下操作:
- 通过
.parents
属性可以跳过最前的上下文,d.parents
等价于ChainMap(*d.maps[1:])
; - 通过
.new_child()
方法在最前快速创建“子上下文”,d.new_child()
等价于ChainMap({}, *d.maps)
。
因为创建 ChainMap
的开销很低,这两个属性或方法都会得到新的 ChainMap
对象。
参考资料/延伸阅读
Mapping Types: dict — Built-in Types — Python 3 documentation
collections — Python Standard Library — Python 3 documentation