第3章 字典和集合
#《流畅的Python》读书笔记 # 第3章 字典和集合 # 字典这个数据结构活跃在所有 Python 程序的背后,即便你的源码里并没有直接用到它。 # 正是因为字典至关重要,Python对它的实现做了高度优化,而散列表则是字典类型性能出众的根本原因。 # 本章内容的大纲如下: # 常见的字典方法 # 如何处理查找不到的键 # 标准库中 dict 类型的变种 # set 和 frozenset 类型 # 散列表的工作原理 # 散列表带来的潜在影响(什么样的数据类型可作为键、不可预知的顺序,等等) # 3.1 泛映射类型 # 如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 __hash__() 方法。 # 另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的…… # 原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散列的,因为根据其定义,frozenset 里只能容纳可散列类型。 # 元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。 # 来看下面的元组tt、tl 和 tf: >>> tt = (1, 2, (30, 40)) >>> hash(tt) >>> tl = (1, 2, [30, 40]) >>> hash(tl) Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> hash(tl) TypeError: unhashable type: 'list' >>> tf = (1, 2, frozenset([30, 40])) >>> hash(tf) # 根据这些定义,字典提供了很多种构造方法,“Built-in Types” # >>> 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}) # >>> a == b == c == d == e # True # 3.2 字典推导 # 自Python2.7以来,列表推导和生成器表达式的概念就移植到了字典上,从而有了字典推导。 # 示例 3-1 字典推导的应用 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)#{'Bangladesh': 880, 'Russia': 7, 'Nigeria': 234, 'Brazil': 55, 'United States': 1, 'Indonesia': 62, 'India': 91, 'Japan': 81, 'China': 86, 'Pakistan': 92} print({code:country.upper() for country,code in country_code.items() if code < 66})#{1: 'UNITED STATES', 55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA'} # 3.3 常见的映射方法 # 映射类型的方法其实很丰富。表3-1为我们展示了dict/defaultdict和OrderDict的常见方法,后面两个数据类型是dict的变种,位于collections模块内。 # 用setdefault处理找不到的键 # 当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的“快速失败”哲学。 # 示例 3-2 index0.py 这段程序从索引中获取单词出现的频率信息,并把它们写进对应的列表里(更好的解决方案在示例3-4中) 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) # 这其实是一种很不好的实现,这样写只是为了证明论点 # ❶ 提取 word 出现的情况,如果还没有它的记录,返回 []。 occurrences=index.get(word,[]) # ❷ 把单词新出现的位置添加到列表的后面。 occurrences.append(location) # ❸ 把新的列表放回字典中,这又牵扯到一次查询操作。 index[word]=occurrences # 以字母顺序打印出结果 # ❹ sorted函数的key = 参数没有调用str.uppper,而是把这个方法的引用传递给sorted函数,这样在排序的时候,单词会被规范成统一格式。 for word in sorted(index,key=str.upper): print(word,index[word]) # 示例 3-3 这里是示例3-2的不完全输出,每一行的列表都代表一个单词的出现情况,列表中的元素是一对值,第一个值表示出现的行,第二个表示出现的列 # 示例 3-4 index.py用一行就解决了获取和更新单词的出现情况列表,当然跟示例3-2不一样的是,这里用到了dict.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 映射的弹性键查询 # 有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。 # 有两个途径能帮我们达到这个目的,一个是通过defaultdict这个类型而不是普通的dict,另一个是给自己定义一个dict的子类,然后在子类中实现__missing__方法。 # 3.4.1 defaultdict:处理找不到的键的一个选择 # 具体而言,在实例化一个defaultdict的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在__getitem__碰到找不到的键的时候被调用,让__getitem__返回默认值。 # 示例 3-5 index_default.py:利用 defaultdict 实例而不是setdefault 方法 # 3.4.2 特殊方法__missing__ # 所有的映射类型在处理找不到的键的时候,都会牵扯到 __missing__方法。 # 这也是这个方法称作“missing”的原因。 # 虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。 # 也就是说,如果有一个类继承了 dict,然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。 # 示例 3-6 当有非字符串的键被查找的时候,StrKeyDict0 是如何在该键不存在的情况下,把它转换为字符串的 # 示例 3-7 StrKeyDict0 在查询的时候把非字符串的键转换为字符串 # 3.5 字典的变种 # 这一节总结了标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型。 # collections.OrderedDict这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。 # collections.ChainMap该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。 # colllections.UserDict这个类其实就是把标准 dict 用纯 Python 又实现了一遍。 # collections.Counter这个映射类型会给键准备一个整数计数器。 # 下面的小例子利用Counter来计算单词中各个字母出现的字数: >>> import collections >>> 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)] # 3.6 子类化UserDict # 就创造自定义映射类型来说,以UserDict为基类,总比以普通的dict为基类要来得方便。 # 而更倾向于从 UserDict 而不是从 dict 继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这些问题。 # 示例 3-8 无论是添加、更新还是查询操作,StrKeyDict都会把非字符串的键转换为字符串 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 # 3.7不可变映射类型 # 标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误地修改某个映射。 # 从 Python 3.3 开始,types 模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。 # 示例 3-9 用MappingProxyType来获取字典的只读实例mappingproxy >>> 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 "<pyshell#5>", line 1, in <module> d_proxy[2] = 'x' TypeError: 'mappingproxy' object does not support item assignment >>> d[2] = 'B' >>> d_proxy mappingproxy({1: 'A', 2: 'B'}) >>> d_proxy[2] 'B' # 3.8 集合论 # 本书中“集”或者“集合”既指 set,也指 frozenset。当“集”仅指代 set 类时,我会用等宽字体表示 。 # 集合的本质是许多唯一对象的聚集。因此,集合可以用于去重: >>> l = ['spam', 'spam', 'eggs', 'spam'] >>> set(l) {'spam', 'eggs'} >>> list(set(l)) ['spam', 'eggs'] # 除了保证唯一性,集合还实现了很多基础的中缀运算符。给定两个集合a 和 b,a | b 返回的是它们的合集,a & b 得到的是交集,而 a - b得到的是差集。 # 例如,我们有一个电子邮件地址的集合(haystack),还要维护一个较小的电子邮件地址集合(needles),然后求出needles中有多少地址同时也出现在了heystack里。 # 借助集合操作,我们只需要一行代码就可以了(见示例 3-10)。 # 示例 3-10 needles 的元素在haystack里出现的次数,两个变量都是set类型 # found = len(needles & haystack) # 如果不使用交集操作的话,代码可能就变成了示例 3-11 里那样。 # 示例 3-11 needles 的元素在 haystack 里出现的次数(作用和示例 3-10 中的相同) found = 0 for n in needles: if n in haystack: found += 1 # 示例 3-12needles的元素在haystack里出现的次数,这次的代码可以用在任何可迭代对象上 # found = len(set(needles) & set(haystack)) # 另一种写法: # found = len(set(needles).intersection(haystack)) # 3.8.1 集合字面量 # 除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的数学形式一模一样。如果是空集,那么必须写成 set() 的形式。 # 句法的陷阱:不要忘了,如果要创建一个空集,你必须用不带任何参数的构造方法 set()。 # 如果只是写成 {} 的形式,跟以前一样,你创建的其实是个空字典。 # 用 dis.dis(反汇编函数)来看看两个方法的字节码的不同: from dis import dis print(dis('{1}')) # 1 0 LOAD_CONST 0(1) # 3 BUILD_SET 1 # 6 RETURN_VALUE # None print(dis('set[1]')) # 1 0 LOAD_NAME 0 (set) # 3 LOAD_CONST 0 (1) # 6 BINARY_SUBSCR # 7 RETURN_VALUE # None # 3.8.2 集合推导 # 示例 3-13 新建一个Latin-1字符集合,该集合里的每个字符的Unicode名字里都有“SIGN”这个单词 from unicodedata import name print({chr(i) for i in range(32,256) if 'SIGN' in name(chr(i),'')}) #{'§', '<', '>', '¤', '=', '¢', '¥', '$', '©', '+', '#', '°', '%', '¬', '±', 'µ', '¶', '×', '÷', '£', '®'} # 3.8.3 集合的操作 # 表3-2:集合的数学运算: # 表3-3:集合的比较运算符,返回值是布尔类型 # 表3-4:集合类型的其他方法 # 3.9 dict和set的背后 # 想要理解 Python 里字典和集合类型的长处和弱点,它们背后的散列表是绕不开的一环。 # 3.9.1 一个关于效率的实验 # 所有的 Python 程序员都从经验中得出结论,认为字典和集合的速度是非常快的。接下来我们要通过可控的实验来证实这一点。 # 示例 3-14 在 haystack 里查找 needles 的元素,并计算找到的元素的个数 found = 0 for n in needles: if n in haystack: found += 1 # 示例 3-15 利用交集来计算 needles 中出现在 haystack 中的元素的个数 # found = len(needles & haystack) # 3.9.2 字典中的散列表 # 因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。 # 示例 3-16 在32 位的 Python 中,1、1.0001、1.0002 和 1.0003这几个数的散列值的二进制表达对比 # 3.9.3 dict的实现及其导致的结果 # 3.9.4 set的实现以及导致的结果 # 集合里的元素必须是可散列的。 # 集合很消耗内存。 # 可以很高效地判断元素是否存在于某个集合。 # 元素的次序取决于被添加到集合里的次序。 # 往集合里添加元素,可能会改变集合里已有元素的次序。 # 3.10 本章小结 # 大多数映射类型都提供了两个很强大的方法:setdefault 和update。 # setdefault方法可以用来更新字典里存放的可变值(比如列表),从而避免了重复的键搜索。 # update方法则让批量更新成为可能,它可以用来插入新值或者更新已有键值对,它的参数可以是包含(key, value)这种键值对的可迭代对象,或者关键字参数。 # 在映射类型的API中,有个很好用的方法是__missing__,当对象找不到某个键的时候,可以通过这个方法自定义会发生什么。 # 3.11 延伸阅读