larken

勤奋的人生才有价值

导航

第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 延伸阅读

 

posted on 2019-03-14 22:52  larken  阅读(202)  评论(0编辑  收藏  举报