第3章 字典和集合
3.1 泛映射类型
collections.abc
模块中有Mapping
和MutableMapping
两个抽象基类, 为dict
和其他类似的类型定义形式接口.
非抽象映射类型一般不直接继承这些抽象基类, 他们会直接对dict
或collections.Userdict
进行扩展. 这些抽象基类的主要作用是作为形式化的文档, 它们定义了构建一个映射类型所需要的最基本的借口.
from collections import abc
my_dict = {}
isinstance(my_dict, abc.Mapping)
True
标准库里的所有映射类型都是利用dict
来实现的, 他们的共同限制是可散列的数据类型才能作为映射里的键.
原子不可变数据类型(str
,byte
和数值类型)都是可散列类型, frozenset
也是可散列的. 元组中的所有元素都是可散列类型时, 元组是可散列的.
3.2 字典推导
字典推导可以从任何以键值对作为元素的可迭代对象中构建出字典.
isinstance(my_dict, abc.Mapping)
#%%
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}
country_code
{'China': 86,
'India': 91,
'United States': 1,
'Indonesia': 62,
'Brazil': 55,
'Pakistan': 92,
'Bangladesh': 880,
'Nigeria': 234,
'Russia': 7,
'Japan': 81}
{code : country.upper() for country, code in country_code.items() if code < 66}
{1: ‘UNITED STATES’, 62: ‘INDONESIA’, 55: ‘BRAZIL’, 7: ‘RUSSIA’}
3.3 常见的映射方法
在映射对象的方法里, setdefault
是比较微妙的一个.
当字典d[k]
找不到正确的键时, Python会抛出异常. 可以使用d.get(k, default)
来代替d[k]
, 给找不到的键一个默认的返回值. 然而这不是最好的方法.
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)
# bad example
occurrences = index.get(word, [])
occurrences.append(location)
index[word] = occurrences
for word in sorted(index, key=str.upper):
print(word, index[word])
上述的这种写法是一种不好的写法, 用setdefault
可以更好的解决
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])
这种情况下, 如果一个单词不存在, 会把单词和一个空列表放进映射.
3.4 映射的弹性键查询
3.4.1 defaultdict
: 处理找不到的键的一个选择
在实例化一个defaultdict
时, 需要给构造方法一个可调用对象, 这个可调用对象会在__getitem__
碰到找不到的键的时候被调用.
import collections
import sys
import re
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
.
这里的default_factory
只有在__getitem__
时会被调用, 其他方法如get
时则不会被调用.
3.4.2 特殊方法__missing__
__missing__
方法同样只会在__getitem__
时调用.
class StrKeyDict0(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()
from strkeydict0 import StrKeyDict0
d = StrKeyDict0([('2', 'two'), ('4', 'four')])
d['2']
‘two’
d[4]
‘four’
d[1]
KeyError: ‘1’
d.get('2')
‘two’
d.get(4)
‘four’
d.get(1,' N/A')
’ N/A’
2 in d
True
1 in d
False
3.5 字典的变种
collections.OrderedDict
: 添加键的时候会保持顺序.
collections.ChainMap
: 容纳数个不同的映射对象.
collections.Counter
: 给键一个整数计数器. most_common
方法会返回最常见的前n个键以及他们的计数.
collections.UserDict
把标准dict
用纯python实现了一遍.
3.6 子类化UserDict
就创建自定义映射类型来说, 以UserDict
为基类更方便.
从dict
继承不好是因为dict
有时会在某些方法上走捷径, 导致我们要在其子类中重写这些方法.
UserDict
并不是dict
的子类, 但它的一个属性data
是dict
的实例. 这个属性实际上是UserDict
最终存储数据的地方.
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
UserDict
继承的是MutableMapping
, 所以StrKeyDict
剩下的那些映射类型的方法都是从UserDict
, MutableMapping
, Mapping
这些超类继承而来的.
3.7 不可变映射类型
types
模块中有一个封装类名叫MappingProxyType
, 如果给这个类一个映射, 它会返回一个只读的映射视图.
from types import MappingProxyType
d = {1: 'A'}
d_proxy = MappingProxyType(d)
print(d_proxy)
print(d_proxy[1])
d_proxy[2] = 'x'
{1: ‘A’}
A
TypeError: ‘mappingproxy’ object does not support item assignment
d[2] = 'B'
print(d_proxy)
print(d_proxy[2])
{1: ‘A’, 2: ‘B’}
B
3.8 集合论
集合可以用来去重
l = ['spam', 'spam', 'eggs', 'spam']
print(set(l))
print(list(set(l)))
{‘spam’, ‘eggs’}
[‘spam’, ‘eggs’]
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b)
print(a & b)
print(a - b)
{1, 2, 3, 4}
{2, 3}
{1}
上述例子展示了集合的交, 并, 差运算.
查找needles
在haystack
中出现的次数, 可以用
found = len(needles & haystack)
3.8.1 集合字面量
如果是空集, 必须写成set()
的形式, 否则可以写成{1, 2}
的形式.
如果写成{}
, 创建的其实是一个空字典.
s = {1}
print(type(s))
print(s)
print(s.pop())
print(s)
<class ‘set’>
{1}
1
set()
{1, 2, 3}
这样的字面量句法要比构造方法set([1, 2, 3])
更快, 通过dis.dis
可以查看字节码.
from dis import dis
dis('{1}')
1 0 LOAD_CONST 0 (1)
2 BUILD_SET 1
4 RETURN_VALUE
dis('set([1])')
1 0 LOAD_NAME 0 (set)
2 LOAD_CONST 0 (1)
4 BUILD_LIST 1
6 CALL_FUNCTION 1
8 RETURN_VALUE
frozenset
没有特殊字面量句法, 只能采用构造方法.
frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
3.8.2 集合推导
from unicodedata import name
{chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}
{‘#’,
‘$’,
‘%’,
‘+’,
‘<’,
‘=’,
‘>’,
‘¢’,
‘£’,
‘¤’,
‘¥’,
‘§’,
‘©’,
‘¬’,
‘®’,
‘°’,
‘±’,
‘µ’,
‘¶’,
‘×’,
‘÷’}
3.9 dict
和set
背后
3.9.1 一个关于效率的实验
在字典和集合中搜索要比在列表中快很多.
3.9.2 字典中的散列表
散列表其实是一个稀疏数组, 其单元通常称为表元. 在dict
的散列表中, 每个键值对都占用一个标原, 每个表元有两个部分, 对键的引用和对值的引用. 所有表元大小一致, 因此可以用偏移量读取某个单元.
Python会设法保证大概三分之一的表元是空的, 所以快达到阈值时, 原有散列表会被复制一个更大的空间里面.
要将一个对象放入散列表, 首先要计算这个元素键的散列值. hash()
可以用来计算.
1.散列值和相等性
要想散列值能够胜任散列表索引这一角色, 他们必须在索引空间尽量分散, 越是相似的对象, 散列值的差异应越大.
2.散列表算法
要获取my_dict[search_key]
的值, Python首先调用hash(search_key)
计算其散列值, 把这个值最低几位当作偏移量, 在散列表里查找表元.
若表元为空, 则抛出KeyError
异常. 若不为空, 则检查search_key==found_key
, 若相等则返回found_value
. 若不等, 则在散列值中另外取几位, 再次尝试.
添加新元素只需在发现空表元时放入新元素. 更新现有键值在找到相对应表元后, 原值会被替换成新值.
3.9.3dict
的实现及其导致的后果
1.键必须是可散列的
一个可散列对象需满足以下要求
(1) 支持hash
函数, 并且通过__hash__
方法得到的散列值是不变的.
(2) 支持通过__eq__
方法检测相等性
(3) a==b
为真, hash(a)==hash(b)
为真
2.字典在内存上开销巨大
如果要存放数量巨大的记录, 那么放在由元组或是具名元组构成的列表中会是一个比较好的选择.
3.键查询快
4.键的次序取决于添加顺序
dict
中添加新键发生散列冲突时, 新键可能会被安排存放到另一位置. 因此在dict([(key1, value1), (key2, value2)])
的比较中得到的结果是相等.
5.往字典添加新键可能改变已有键的顺序
无论何时往字典添加新键, Python都可能做出字典扩容的决定,这个过程可能产生新的散列冲突, 导致新散列表中键的次序变化.
因此, 不要对字典同时进行迭代和修改.
3.9.4 set
的实现以及导致的后果
集合里的元素是可散列的
集合很消耗内存
可以高效判断元素是否存在于某个集合
元素的次序取决于被添加到集合的次序
往集合里添加元素, 可能会改变已有元素的次序