dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在__builtins__.__dict__模块中。
标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的数据类型)。
字典推导
from collections.abc import Mapping, MutableMapping
my_dict = {}
print(isinstance(my_dict, Mapping))
# 字典推导
DIAL_CODES = [
(86, 'China'),
(91, 'India'),
(1, 'United States'),
(62, 'Indonesia'),
(55, 'Brazil'),
(92, 'Pakistan'),
(880, 'Bangladesh'),
(234, 'Nigeria'),
(7, 'Russia'),
(81, 'Japan'),
]
n_dic = {key.upper():value for value,key in DIAL_CODES if value < 66}
print(n_dic)
常见的映射方法
用setdefault处理找不到的键
- 当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的“快速失败”哲学。也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k],给找不到的键一个默认的返回值(这比处理 KeyError 要方便不少)。但是要更新某个键对应的值的时候,不管使用 getitem 还是 get 都会不自然,而且效率低。
"""创建一个从单词到其出现情况的映射"""
import sys
import re
WORD_RE = re.compile(r'\w+')
index = {}
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)
# 这其实是一种很不好的实现,这样写只是为了证明论点
occurrences = index.get(word, [])
occurrences.append(location)
index[word] = occurrences
# 以字母顺序打印出结果
for word in sorted(index, key=str.upper):
print(word, index[word])
# ===========================================================================
"""创建从一个单词到其出现情况的映射"""
import sys
import re
WORD_RE = re.compile(r'\w+')
index = {}
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.setdefault(word, []).append(location) ➊
# 以字母顺序打印出结果
for word in sorted(index, key=str.upper):
print(word, index[word])
# dict.setdefault方法是在原字典修改
映射的弹性键查询
- 有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现__missing__ 方法。
defaultdict:处理找不到的键的一个选择
有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的,
一个是通过 defaultdict 这个类型而不是普通的 dict,
另一个是给自己定义一个 dict 的子类,然后在子类中实现__missing__ 方法。
- defaultdict
collections.defaultdict 在创建的时候,需要给他配置一个找不到键创造默认值的方法,
在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在 __getitem__ 碰到找不到的键的时候被调用,让 __getitem__ 返回某种默认值
-- 比如,我们新建了这样一个字典:dd=defaultdict(list),如果键 'new-key' 在 dd中还不存在的话,表达式 dd['new-key'] 会按照以下的步骤来行事。
(1) 调用 list() 来建立一个新列表。
(2) 把这个新列表作为值,'new-key' 作为它的键,放到 dd 中。
(3) 返回这个列表的引用。
"""创建一个从单词到其出现情况的映射"""
import sys
import re
import collections
WORD_RE = re.compile(r'\w+')
index = collections.defaultdict(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)
for word in sorted(index, key=str.upper):
print(word, index[word])
'''
把 list 构造方法作为 default_factory 来创建一个 defaultdict。
如果 index 并没有 word 的记录,那么 default_factory 会被调用,为查询不到的键创造一个值。
这个值在这里是一个空的列表,然后这个空列表被赋值给 index[word],继而被当作返回值返回,
因此append(location) 操作总能成功
'''
# 如果在创建 defaultdict 的时候没有指定 default_factory,查询不存在的键会触发KeyError。
'''
所有这一切背后的功臣其实是特殊方法 __missing__。它会在 defaultdict 遇到找不到的键
的时候调用 default_factory,而实际上这个特性是所有映射类型都可以选择去支持的。
'''
特殊方法__missing__
- 所有的映射类型在处理找不到的键的时候,都会牵扯到 missing 方法。 __missing__方法只会被__getitem__调用,
# MyDict在查询的时候把非字符串的键转换为字符串
class MyDict(dict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str[key]]
def get(self, key, default = None):
try:
return self[key]
except KeyError:
return default
def __contains__(self, key):
return key in self.keys() or str(key) in self.keys()
字典的变种*
- collections.OrderedDict
-这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。 - collections.ChainMap
-该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。在 collections文档介绍 ChainMap 对 象 的 那 一 部 分(https://docs.python.org/3/library/collections.html# collections.ChainMap)里有一些具体的使用示例,其中包含了下面这个 Python 变量查询规则的代码片段:
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))
- collections.Counter
-这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。Counter 实现了 + 和 - 运算符用来合并记录,还有像 most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最常见的 n 个键和它们的计数,详情参阅文档(https://docs.python.org/3/library/collections.html#collections.Counter)。下面的小例子利用 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(2)
[('a', 10), ('z', 3)]
- colllections.UserDict
-这个类其实就是把标准 dict 用纯 Python 又实现了一遍。
不可变映射类型:
- python3.3开始,types模块中引入了一个封装类名叫MappingProxyType 如果给这个类一个映射,它会返回一个只读的映射视图。如果对原映射做了改动,通过整个视图可以观察到。
>>> from types import MappingProxyType
>>> d = {1:'a'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({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[2] = 'c'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d_proxy[2]
'b'
>>>
MappingProxyType不可被修改
集合论
- 集合的本质是许多唯一对象的聚集,所以可以被用来去重。
- 集合的元素必须是可散列的,但是frozenset本身是可以散列的,所以可以创建一个包含frozenset的set。
- 集合实现了很多基础的中缀运算符
给定集合a和b
a|b 返回的是他们的合集
a&b 返回的是他们的交集
a-b 是差集
集合字面量
- 除空集外,集合的字面量 —{1},{1,2,3} ,如果是空集,那么必须写成set()形式,如果写成{} 则解释器认为是一个空字典。
集合推导
{i for i in range(10) if i % 2 == 0}
集合的操作
dict和set的背后
- Python 里的 dict 和 set 的效率有多高?
- 为什么它们是无序的
- 为什么并不是所有的对象都可以当作dict的键或set里的元素。
- 为什么dict的键和set元素的顺序是根据它们被添加的次序而定的,以及为什么在映射对象的生命周期中,整个顺序并不是一成不变的。
- 为什么不应该在迭代循环dict或是set的同时往里添加元素。
字典中的散列表
- 散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。散列表里的单元通常叫做表元。在dict的散列表中,每个键值对都占用一个表元,每个表元都有俩个部分,一个是对键的引用,另一个是对值的引用。因为所有的表元大小都一直。可以通过偏移量来读取某个表元。
- 因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
- 如果要把一个对象放入散列表,那么首先要计算这个元素键的散列表。python中可以用hash()做这个事。
- 内置的hash可以用于任何的内置对象。如果俩个对象在比较的时候是相等的,那么他们的散列也必须相等,例如:1==1.0 为true,那么hash(1) == hash(1.0) 也必须为true,
- 为了让散列值能够胜任散列表索引这一角色,他们必须在索引空间中尽量分散开来,(在理想的情况下,越是不相等的对象,他们的散列值的差别应该也越大)
- 散列表算法
为了获取 my_dict[search_key] 背后的值,Python 首先会调hash(search_key)计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出KeyError 异常。若不是空的,则表元里会有一对found_key:found_value。这时候 Python 会检验 search_key== found_key 是否为真,如果它们相等的话,返回 found_value。
如果 search_key 和 found_key 不匹配的话,这种情况称为散列冲突。发生这种情况是因为。散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。若这次找到的表元是空的,则同样抛出 KeyError;若非空,或者键匹配,则回这个值;或者又发现了散列冲突,则重复以上的步骤。
dict的实现及其导致的结果
散列给dict带来的优势和限制
- 键必须是可散列的
一个可散列的对象必须满足以下需求
1)支持hash函数,并通过__hash__()方法所得到的散列值是不可变的。
2)支持通过__eq__()方法检测相等性。
3)若a == b 为真,则hash(a) == hash(b) 也为真。
所有用户自定义的对象默认都是可散列的,因为它们的散列值由id()来获取,而且它们不相等。 - 字典在内存上的开销巨大
因为字典运用了散列表,而散列表又必须是稀疏的,导致需要占用很大空间。
如果需要存放巨大的记录,那么放在元组或者具名元组中比较好。
用元组的原因:
1)避免了散列表耗费的空间
2)不需要把记录中字段的名字在每个元素里都存一遍。 - 键查询很快
dict是典型的空间换时间: - 建的次序取决于添加顺序
当往dict中添加值的时候,可能发生散列冲突,新的键可能会被放到另一个位置。 - 往字典里添加键可能会改变已有键的顺序
无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。要注意的是,上面提到的这些变化是否会发生以及如何发生,都依赖于字典背后的具体实现,如果你在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环很有可能会跳过一些键——甚至是跳过那些字典中已经有的键。由此可知,不要对字典同时进行迭代和修改。
如果想扫描并修改一个字典,最好分成两步来进行:
1)首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;
2)迭代结束之后再对原有字典进行更新。
set的实现以及导致的结果
set和frozenset的实现也依赖散列表,但是它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)
- 字典和散列的几个特点也适用于集合
1)集合里的元素必须是可散列的
2)集合很消耗内存
3)可以高效的判断元素是否存在于某个集合
4)元素的次序取决于被添加到集合里的次序。
5)往集合里添加元素,可能会改变集合里已有的元素的次序。
总结
- 字典算得上是python的基石,python提供了很多特殊映射类型,比如:defaultdict,OrderedDict,ChainMap和Counter,这些类型都属于collections模块。以及可以扩展的UserDict类
- 大多数映射类型都提供了俩个很强大的方法,setdefault,和update,setdefault方法可以用来更新字典的可变值(比如列表),避免了重复的键搜索。update则是提供了批量更新。
- __missing__当对象找不到键的时候可以通过这个方法自定义返回什么。
- colections.abc 模块提供了Mapping 和 MutableMapping 这两个抽象基类。可以进行类型查询或者引用。
- MappingProxyType 可以用来创建不可变映射对象,它被封装在 types 模块中。另外还有 Set 和MutableSet 这两个抽象基类
- dict 和 set 背后的散列表效率很高,被保存的元素会呈现出不同的顺序,以及已有的元素顺序会发生变化。