这章主要介绍字典和集合。
首先讲了一下字典创建的语法,像列表推导,字典也有字典推导,里面对于迭代的元素通过 key:value 这样的形式生成:
>>> dial_codes = [
... (880, 'Bangladesh'),
... (55, 'Brazil'),
... (86, 'China'),
... (91, 'India'),
... (62, 'Indonesia'),
... (81, 'Japan'),
... (234, 'Nigeria'),
... (92, 'Pakistan'),
... (7, 'Russia'),
... (1, 'United States'),
... ]
>>> country_dial = {country: code for code, country in dial_codes}
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62,
'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
>>> {code: country.upper()
... for country, code in sorted(country_dial.items())
... if code < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}
对于字典的拆包,需要 ** 在变量前面:
>>> def dump(**kwargs):
... return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}
python 3.9 的新特性是支持 | 和 |= 的归并操作:
>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}
|= 同样符合增量运算的规则,左边的变量会被赋值:
>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}
模式匹配同样适用于字典类型:
def get_creators(record: dict) -> list:
match record:
case {'type': 'book', 'api': 2, 'authors': [*names]}:
return names
case {'type': 'book', 'api': 1, 'author': name}:
return [name]
case {'type': 'book'}:
raise ValueError(f"Invalid 'book' record: {record!r}")
case {'type': 'movie', 'director': name}:
return [name]
case _:
raise ValueError(f'Invalid record: {record!r}')
>>> b1 = dict(api=1, author='Douglas Hofstadter',
... type='book', title='Gödel, Escher, Bach')
>>> get_creators(b1)
['Douglas Hofstadter']
>>> from collections import OrderedDict
>>> b2 = OrderedDict(api=2, type='book',
... title='Python in a Nutshell',
... authors='Martelli Ravenscroft Holden'.split())
>>> get_creators(b2)
['Martelli', 'Ravenscroft', 'Holden']
>>> get_creators({'type': 'book', 'pages': 770})
Traceback (most recent call last):
...
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
>>> get_creators('Spam, spam, spam')
Traceback (most recent call last):
...
ValueError: Invalid record: 'Spam, spam, spam'
跟序列的模式匹配的区别在于未匹配上的 key 不会在匹配的判断条件上,所以,如果有不关心的key,不用放在 case 的语句里,或者用 **variable 和 **_ 来获取:
>>> 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}
字典继承了 Mapping 和 MutableMapping 这两个抽象基类,这里主要提到 当创建自定义的类似字典类的时候,继承 collections.UserDict 要更好一些,之后也会具体讲原因。
可被哈希指的是一个对象通过哈希运算得到的值在其生命周期内不会改变,并且可以与其他的对象的哈希值比较。这里提到了不会改变,所以对象也必须是不可变的,比如数字,str,bytes 这种平铺序列,不可变容器序列而且其内部的元素也要满足可被哈希。
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110
对于用户自定义的类,默认都是可被哈希的,以为哈希算法使用 id() 这个方法,__eq__ 这个特殊方法从 object 类继承,比较的就是 id(),自己修改了特殊方法后,满足 __hash__ 和 __eq___ 的结果不可变的话,也属于可哈希的。
接下来讨论的是向字典插入或者更新可变的 values,先给了一个非常普通的例子:
"""Build an index mapping word -> list of occurrences"""
import re
import sys
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)
# this is ugly; coded like this to make a point
occurrences = index.get(word, [])
occurrences.append(location)
index[word] = occurrences
# display in alphabetical order
for word in sorted(index, key=str.upper):
print(word, index[word])
可用 dict.setdefault 优化一下:
"""Build an index mapping word -> list of occurrences"""
import re
import sys
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)
# display in alphabetical order
for word in sorted(index, key=str.upper):
print(word, index[word])
更好的方式也可以自动的处理缺少的 key
一种方法是用 defaultdict 这个类,如下:
"""Build an index mapping word -> list of occurrences"""
import collections
import re
import sys
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)
# display in alphabetical order
for word in sorted(index, key=str.upper):
print(word, index[word])
defaultdict 的实现原理 通过 __missing__ 这个特殊方法,当 __getitem__ 这个特殊方法找不到 key 的时候,会调用 __missing__:
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()
在 __missing__ 里面判断 key 的类型是必须的,如果没有,get 会调 __missing__, __missing__ 内部也会调 get,这样一直循环下去。
需要注意的是,在上个例子里,__contains__ 这个方法不会触发 __missing__,因为其内部是通过 key in self.keys() 这样查找的。
__missing__ 的调用在标准库里面有点不一致,对于 dict 和 collections.UserDict 这两个类的子类,实现了 __missing__ 会被 d[k] 或者 d.get(k),没找到时自动调用,对于 abc.Mapping 类的子类,除非 __getitem__ 内部调用 __missing__, 不然 __missing__ 不会自动调用。
之后讲了下字典的变种,就像之前的 defaultdict 这样的类。
首先是 collections.OrderedDict, 这个类有几个特点:
- 比较字典的时候会比较内部 kv 顺序
- popitem() 可以指定位置弹出
- move_to_end() 可以换 kv 位置
- 比 dict 原始 字典要在排序方面不仅性能好,而且提供了更多 api
collections.ChainMap 故名思意就是其内部包含多个字典,但是实际操作中,当成一个字典来看待:
>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6
ChainMap 内部保留的是引用,并且对于 ChainMap 的更新插入操作只对其内部第一个字典,主要用处就是在写解释器的时候嵌套的scopes时:
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))
之后是 collections.Counter, 主要是对每个 key 进行一个计数统计:使用例子
>>> 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(3)
[('a', 10), ('z', 3), ('b', 2)]
shelve.Shelf 类似一个数据库那样的形式,提供了一些 I/O 相关的操作,比如 sync 和 close,因为这个所以属于上下文管理器,对于同一个 key 的 value 不会覆盖之前的值,key 必须是 字符串,值的化 必须能够被 pickle 模块序列化才行。
继承 collections.UserDict 比 原生 dict 好处在于一些内置方法的重写上,先对比一下效果:
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 扩展了 abc.MutableMapping 的一些 方法,结合 abc.Mapping:
MutableMapping.update 可以 更新一个 kv 的迭代器 到字典里,Mapping.get 不用自己单独实现了。
之后介绍一个不可变的字典 MappingProxyType
使用方法可以看下例子:
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[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
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>
原生字典 dict 的 .keys(), .values(), .items() 返回分别是 dict_keys, dict_values, dict_items 类:
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()
>>> values
dict_values([10, 20, 30])
>>> len(values)
3
>>> list(values)
[10, 20, 30]
>>> reversed(values)
<dict_reversevalueiterator object at 0x10e9e7310>
>>> values[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable
这些类的值会根据 字典的改变立即改变,接上面这个例子:
>>> d['z'] = 99
>>> d
{'a': 10, 'b': 20, 'c': 30, 'z': 99}
>>> values
dict_values([10, 20, 30, 99])
可见 这三个类是内部的,它们不会每次新建,而且就连使用者也不能新建:
>>> values_class = type({}.values())
>>> v = values_class()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create 'dict_values' instances
之后总结了几点 dict 的设计:
- keys 必须可被哈希,意味着需要实现 __hash__ 和 __eq__ 方法
- key 的查找 查值非常快
- key 在 python 3.7 会保持有序
- key 的内存占用比较大
- 为了节省内存,避免在 __init__ 之外创建实例属性
对于最后一点,意思就是有字典来维护实例属性,如果 __init__ 外创建实例属性,那么就会用不同的字典来保存,大概是这样子。
讲完字典改讲集合了,集合的第一个特点就是去重:
>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l))
['eggs', 'spam', 'bacon']
set 内部的元素必须可被哈希,set 本身不可被哈希,但是 frozenset 这个类可以。
set 也支持 中缀操作计算,比如 | 是合集,& 是交集, - 是差集,^是反差集。
set 字面的空集 只能用 set() 来表示,用来跟 字典区分:
>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1
>>> s
set()
初始化字典 {1,2,3} 比 set([1,2,3]) 快因为不用建一个 list。
集合也有集合推导,把列表推导 的 [] 改成 {} 即可。
总结了一下集合的使用:
- 跟 字典的 key 一样,元素必须是 hashable 的
- 查找性能很强
- 内存占用比 array 大
- 元素内部顺序取决于插入顺序,但是不是所有情况
- 插入元素会改变已有的元素顺序
之后介绍了很多 集合的操作,很多,先不具体说了。