泛映射类型
collections.abc
中类的继承关系:
然而,非抽象映射类型一般不会直接继承这些抽象基类,它们会直接对 dict
或是collections.UserDict
进行扩展。
import collections.abc as abc
my_dict = {}
print(isinstance(my_dict,abc.Mapping))
标准库里的所有映射类型都是利用 dict
来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键。
可散列的数据类型,如果一个对象是可散列的:
- 在这个对象的生命周期中,它的散列值是不变的
- 这个对象需要实现
__hash__()
方法 - 可散列对象还要有
__eq__()
方法, 这样才能跟其他键做比较
元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的:
tt = (1,2,(30,40))
print(hash(tt))
tl = (1,2,[30,40])
print(hash(tl)) # TypeError: unhashable type: 'list'
创建 dict
的不同方式:
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
print(a == b == c == d == e)
字典推导
字典推导可以从任何以键值对作为元素的可迭代对象中构建出字典:
DIAL_CODES = [(86, 'China'),
(91, 'India'),
(1, 'United States'),
(62, 'Indonesia'),
(55, 'Brazil'),
(92, 'Pakistan'),
(880, 'Bangladesh'),
(234, 'Nigeria'),
(7, 'Russia'),
(81, 'Japan')
]
country_code = {country : code for code,country in DIAL_CODES}
print(country_code)
country_code = {code : country.upper() for country,code in country_code.items() if code < 66}
print(country_code)
常见的映射方法
当字典 d[k]
不能找到正确的键的时候,python 会抛出异常,这个行为符合 python 所信奉的“快速失败”哲学。
可以用 d.get(k, default)
来代替 d[k]
,给找不到的键一个默认的返回值。
dict = {}
dict.setdefault('name','Bill')
print(dict) # {'name': 'Bill'}
dict.setdefault('name','Mike')
print(dict) # 并没有改变,依然是: {'name': 'Bill'}
print(dict["name"]) # Bill
dict["name"] = "Jim"
print(dict["name"]) # Jim
setdefault
给代码带来简洁性:
my_dict.setdefault(key, []).append(new_value)
# 等价于:
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)
映射的弹性键查询
有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的:
- 一个是通过
defaultdict
这个类型而不是普通的dict
- 另一个是给自己定义一个
dict
的子类,然后在子类中实现__missing__
方法
defaultdict
:处理找不到的键的一个选择
在实例化一个 defaultdict
的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在 __getitem__
碰到找不到的键的时候被调用,让 __getitem__
返回某种默认值。defaultdict
接受一个工厂函数作为参数,如下来构造:
dict =defaultdict( factory_function)
比如,我们新建了这样一个字典: dd = defaultdict(list)
,如果键 'new-key'
在 dd
中还不存在的话,表达式 dd[‘new-key’] 会按照以下的步骤来行事:
- 调用
list()
来建立一个新列表 - 把这个新列表作为值,
'new-key'
作为它的键,放到dd
中 - 返回这个列表的引用
如果在创建defaultdict
的时候没有指定default_factory
,查询不存在的键会触发KeyError
。
defaultdict
里的default_factory
只会在__getitem__
里被调用,比如,dd
是个defaultdict
,k
是个找不到的键,dd[k]
这个表达式会调用default_factory
创造某个默认值,而dd.get(k)
则返回None
。
from collections import defaultdict
dict1 = defaultdict(int)
dict2 = defaultdict(set)
dict3 = defaultdict(str)
dict4 = defaultdict(list)
print(dict1[1]) # 0
print(dict2[1]) # set()
print(dict3[1]) # ""
print(dict4[1]) # []
print(dict1.get(0)) # None
特殊方法 __missing__
所有的映射类型在处理找不到的键的时候,都会牵扯到 __missing__
方法。虽然基类 dict
并没有定义这个方法,但是dict
是知道有这么个东西存在的。也就是说,如果有一个类继承了 dict
,然后这个继承类提供了 __missing__
方法,那么在 __getitem__
碰到找不到的键的时候,python 就会自动调用它,而不是抛出一个 KeyError
异常。
__missing__
方法只会被 __getitem__
调用,提供 __missing__
方法对 get
或者 __contains__
方法的使用没有影响。
class StrKeyDict0(dict):
def __missing__(self,key):
# 如果找不到的键本身就是字符串,那就抛出 KeyError 异常
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
collections.OrderedDict
在添加键的时候会保持顺序,因此键的迭代次序总是一致的。
from collections import OrderedDict
od = OrderedDict()
od['name'] = 'egon'
od['age'] = 18
od['gender'] = 'male'
print(od)
# 删除
del od["age"]
# 重新插入,将被置于末尾
od["age"] = 20
print(od)
popitem
方法会删除并返回 (key, value)
对。如果 last
为真,则以 LIFO(后进先出)顺序返回这些键值对,如果为假,则以 FIFO(先进先出)顺序返回:
from collections import OrderedDict
od = OrderedDict()
od['k1'] = 'egon'
od['k2'] = 'tom'
od['k3'] = 'jack'
print(od.popitem(last=False)) # ('k1', 'egon')
print(od.popitem(last=True)) # ('k3', 'jack')
print(od.popitem(last=False)) # ('k2', 'tom')
collections.ChainMap
collections.ChainMap
可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。
from collections import ChainMap
a = {'a': 1, 'b': 2}
b = {'c': 3}
c = ChainMap(a, b)
print(c)
print(c.maps)
collections.Counter
collections.Counter
类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。
from collections import Counter
ct = Counter('abracadabra')
print(ct)
ct.update("aaaaazzzz")
print(ct)
print(ct.most_common(2))
子类化 UserDict
更倾向于从 UserDict
而不是从 dict
继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict
就不会带来这些问题。
UserDict
并不是 dict
的子类,但是 UserDict
有一个叫作 data
的属性,是 dict
的实例,这个属性实际上是 UserDict
最终存储数据的地方。
from collections import UserDict
class StrKeyDict(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
不可变映射类型
如果给 MappingProxyType
一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。
from types import MappingProxyType
d = {1:'A'}
d_proxy = MappingProxyType(d)
print(d_proxy)
# 通过 d_proxy 并不能做任何修改
d_proxy[1] = 'B' # 'mappingproxy' object does not support item assignment
# 对 d 所做的任何改动都会反馈到 d_proxy
d[2] = 'B'
print(d_proxy) # {1: 'A', 2: 'B'}
集合论
集合的本质是许多唯一对象的聚集。因此,集合可以用于去重:
l = ['spam', 'spam', 'eggs', 'spam']
s = set(l)
print(s) # {'eggs', 'spam'}
除了保证唯一性,集合还实现了很多基础的中缀运算符。给定两个集合 a
和 b
, a | b
返回的是它们的合集, a & b
得到的是交集,而 a - b
得到的是差集。
假设有两个集合 haystack
和 needles
,寻找公共元素个数:
found = len(needles & haystack)
# 等价于
found = 0
for n in needles:
for n in haystack:
found += 1
集合字面量
如果是空集,那么必须写成 set()
的形式:
s = {1}
print(type(s))
print(s) # {1}
s.pop()
print(s) # set()
像 {1, 2, 3}
这种字面量句法相比于构造方法 (set([1, 2, 3]))
要更快且更易读。后者的速度要慢一些,因为 python 必须先从 set
这个名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。
from dis import dis
print(dis('{1}'))
"""
1 0 LOAD_CONST 0 (1)
2 BUILD_SET 1
4 RETURN_VALUE
"""
print(dis('set[1]'))
"""
1 0 LOAD_NAME 0 (set)
2 LOAD_CONST 0 (1)
4 BINARY_SUBSCR
6 RETURN_VALUE
"""
集合推导
from unicodedata import name
s = {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')}
print(s)
集合的操作
dict 和 set 的背后
一个关于效率的实验
为了对比容器的大小对 dict
、 set
或 list
的 in
运算符效率的影响,我创建了一个有 1000 万个双精度浮点数的数组,名叫 haystack
。另外还有一个包含了 1000 个浮点数的 needles
数组,其中 500 个数字是从 haystack
里挑出来的,另外 500 个肯定不在 haystack
里。
found = 0
for n in needles:
if n in haystack:
found += 1
在一个有 1000 万个键的字典里查找 1000 个数,花在每个数上的时间不过是 0.337 微秒。
set
和 list
同时测试了 found = len(needles & haystack)
:
字典中的散列表
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。
在 dict
的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为
所有表元的大小一致,所以可以通过偏移量来读取某个表元。
因为 python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。python 中可以用 hash()
方法来做这件事情。
散列值和相等性
内置的 hash()
方法可以用于所有的内置类型对象。如果是自定义对象调用 hash()
的话,实际上运行的是自定义的 __hash__
。
如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。
print(hash(1.0002))
print(hash(1.0001))
散列表算法
my_dict[search_key]
背后的操作原理:
- 首先会调用
hash(search_key)
来计算search_key
的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元 - 若找到的表元是空的,则抛出
KeyError
异常 - 若不是空的,则表元里会有一对
found_key:found_value
。这时候 Python 会检验search_key == found_key
是否为真,如果它们相等的话,就会返回found_value
- 如果
search_key
和found_key
不匹配的话,这种情况称为散列冲突 - 为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元
- 若这次找到的表元是空的,则同样抛出
KeyError
;若非空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复以上的步骤
dict 的实现及其导致的结果
键必须是可散列的:
- 支持
hash()
函数,并且通过__hash__()
方法所得到的散列值是不变的 - 支持通过
__eq__()
方法来检测相等性 - 若
a == b
为真,则hash(a) == hash(b)
也为真
所有由用户自定义的对象默认都是可散列的,因为它们的散列值由 id()
来获取,而且它们都是不相等的。
字典在内存上的开销巨大:
由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。
键查询很快:
dict
的实现是典型的空间换时间:字典类型有着巨大的内存开销,但它们提供了无视数据量大小的快速访问——只要字典能被装在内存里。
键的次序取决于添加顺序:
当往 dict
里添加新键而又发生散列冲突的时候,新键可能会被安排存放到另一个位置。
# 数据元组的顺序是按照国家的人口排名来决定的
d1 = dict(DIAL_CODES)
print('d1:', d1.keys())
# 数据元组的顺序是按照国家的电话区号来决定的
d2 = dict(sorted(DIAL_CODES))
print('d2:', d2.keys())
# 数据元组的顺序是按照国家名字的英文拼写来决定的
d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1]))
print('d3:', d3.keys())
# 这些字典是相等的,因为它们所包含的数据是一样的
assert d1 == d2 and d2 == d3
往字典里添加新键可能会改变已有键的顺序:
无论何时往字典里添加新的键,python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。
set 的实现以及导致的结果
字典和散列表的几个特点,对集合来说几乎都是适用的。为了避免太多重复的内容,这些特点总结如下:
- 集合里的元素必须是可散列的
- 集合很消耗内存
- 可以很高效地判断元素是否存在于某个集合
- 元素的次序取决于被添加到集合里的次序
- 往集合里添加元素,可能会改变集合里已有元素的次序