【读书笔记】 《流畅的python》3.1-3.4 读书笔记

《流畅的python》3.1-3.4 读书笔记

一、泛映射类型(3.1)
collections.abc 模块中有 Mapping 和 MutableMapping 这两个抽象基类,它们的作 用是为 dict 和其他类似的类型定义形式接口(在 Python 2.6 到 Python 3.2 的版本中,这些 类还不属于 collections.abc 模块,而是隶属于 collections 模块)。
非抽象映射类型一般不会直接继承这些抽象基类,它们会直接对 dict 或是 collections.User.Dict 进行扩展。这些抽象基类的主要作用是作为形式化的文档,它 们定义了构建一个映射类型所需要的最基本的接口。然后它们还可以跟 isinstance 一起 被用来判定某个数据是不是广义上的映射类型:

my_dict = {} 
print(isinstance(my_dict, abc.Mapping))

输出 True
这里用 isinstance 而不是 type 来检查某个参数是否为 dict 类型,因为这个参数有可 能不是 dict,而是一个比较另类的映射类型。 标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只有可 散列的数据类型才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的 数据类型)。
什么是可散列的数据类型
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变 的,而且这个对象需要实现 __hash__() 方法。另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……
原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是 可散列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有 当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。

	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 方法,并且在方法中用到了这个对象的内部状态的话,那么只有当所有这些内部状态 都是不可变的情况下,这个对象才是可散列的。
二、字典推导(3.2)
字典推导(dictcomp)可以从任何以键值对作为元素的可 迭代对象中构建出字典。

 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)
print({code: country.upper() for country, code in country_code.items()
if code < 66})   # 跟上面相反,用区域码作为键,国家名称转换为大写,并且过滤掉区域码大于或等于 66 的地区。

输出

{'China': 86, 'India': 91, 'United States': 1, 'Indonesia': 62, 'Brazil': 55, 'Pakistan': 92, 'Bangladesh': 880, 'Nigeria': 234, 'Russia': 7, 'Japan': 81}
{1: 'UNITED STATES', 62: 'INDONESIA', 55: 'BRAZIL', 7: 'RUSSIA'}

三、常见的映射方法(3.3)
dict、defaultdict 和 OrderedDict 的常见方法,后面两个数据类型是 dict 的变种,位于 collections 模块内。

dictdefaultdictOrderedDict
d.clear()移除所有元素
d.__contains__(k)检查 k 是否在 d 中
d.copy()浅复制
d.__copy__()用于支持 copy.copy
d.default_factorymissing 函数中被调用的函数,用以给未找到的 元素设置值😀1
d.__delitem__(k)del d[k],移除键为 k 的元素
d.fromkeys(it, [initial])将迭代器 it 里的元素设置为映射里的键,如果有 initial 参数,就把它作为这些键对应的值(默认是 None)
d.get(k, [default])返回键 k 对应的值,如果字典里没有键 k,则返回 None 或者 default
d.__getitem__(k)让字典 d 能用 d[k] 的形式返回键 k 对应的值
d.items()返回 d 里所有的键值对
d.__iter__()获取键的迭代器
d.keys()获取所有的键
d.__len__()可以用 len(d) 的形式得到字典里键值对的数量
d.__missing__(k)getitem 找不到对应键的时候,这个方法会被调 用
d.move_to_end(k, [last])把键为 k 的元素移动到最靠前或者最靠后的位置(last 的默认值是 True)
d.pop(k, [defaul]返回键 k 所对应的值,然后移除这个键值对。如果没 有这个键,返回 None 或者 defaul
d.popitem()随机返回一个键值对并从字典里移除它😀2
d.__reversed__()返回倒序的键的迭代器
d.setdefault(k, [default])若字典里有键k,则把它对应的值设置为 default,然 后返回这个值;若无,则让 d[k] = default,然后返回 default
d.__setitem__(k, v)实现 d[k] = v 操作,把 k 对应的值设为v
d.update(m, [**kargs])m 可以是映射或者键值对迭代器,用来更新 d 里对应的 条目
d.values()返回字典里的所有值

😀1 .default_factory 并不是一个方法,而是一个可调用对象(callable),它的值在 defaultdict 初始化的时候由用户 设定。
😀2. OrderedDict.popitem() 会移除字典里最先插入的元素(先进先出);同时这个方法还有一个可选的 last 参数,若 为真,则会移除最后插入的元素(后进先出)。
上面的表格中,update 方法处理参数 m 的方式,是典型的“鸭子类型”。函数首先检查 m 是否有 keys 方法,如果有,那么 update 函数就把它当作映射对象来处理。否则,函数 会退一步,转而把 m 当作包含了键值对 (key, value) 元素的迭代器。Python 里大多数 映射类型的构造方法都采用了类似的逻辑,因此你既可以用一个映射对象来新建一个映射 对象,也可以用包含 (key, value) 元素的可迭代对象来初始化一个映射对象。 在映射对象的方法里,setdefault 可能是比较微妙的一个。我们虽然并不会每次都用 它,但是一旦它发挥作用,就可以节省不少次键查询,从而让程序更高效。
用setdefault处理找不到的键
当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉 的“快速失败”哲学。也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k],给找不到的键一个默认的返回值(这比处理 KeyError 要方便不少)。但是要更 新某个键对应的值的时候,不管使用 getitem 还是 get 都会不自然,而且效率低。

"""创建一个从单词到其出现情况的映射""" 
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) 
			# 这其实是一种很不好的实现,这样写只是为了证明论点 
			occurrences = index.get(word, [])  # 提取 word 出现的情况,如果还没有它的记录,返回 []。
			occurrences.append(location)  # 把单词新出现的位置添加到列表的后面。 
			index[word] = occurrences  # 把新的列表放回字典中,这又牵扯到一次查询操作。 
			# 以字母顺序打印出结果 
			for word in sorted(index, key=str.upper):   # sorted 函数的 key= 参数没有调用 str.uppper,而是把这个方法的引用传递给 sorted 函数,这样在排序的时候,单词会被规范成统一格式。 
			print(word, index[word])
"""创建从一个单词到其出现情况的映射""" 
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])

即:

my_dict.setdefault(key, []).append(new_value)

if key not in my_dict: 
	my_dict[key] = [] 
my_dict[key].append(new_value)

二者的效果是一样的,只不过后者至少要进行两次键查询——如果键不存在的话,就是三 次,用 setdefault 只需要一次就可以完成整个操作。
四、映射的弹性键查询
(一)defaultdict:处理找不到的键的一个选择
在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用对象,
这个可调用对象会在 __getitem__ 碰到找不到的键的时候被调用,让 __getitem__ 返回某种默认值。
比如,我们新建了这样一个字典:dd = defaultdict(list),如果键 ‘new-key’ 在 dd中还不存在的话,表达式 dd[‘new-key’] 会按照以下的步骤来行事。
(1) 调用 list() 来建立一个新列表。
(2) 把这个新列表作为值,‘new-key’ 作为它的键,放到 dd 中。
(3) 返回这个列表的引用。
而这个用来生成默认值的可调用对象存放在名为 default_factory 的实例属性里。
defaultdict 里的 default_factory 只会在 __getitem__ 里被调用,在其他的方法里完全不会发挥作用。比如,dd 是个 defaultdict,k 是个找不到的键, dd[k] 这个表达式会调用 default_factory 创造某个默认值,而 dd.get(k)则会返回 None。
__missing__。它会在 defaultdict 遇到找不到的键的时候调用default_factory,而实际上这个特性是所有映射类型都可以选择去支持的
(二)特殊方法__missing__
如果有一个类继承了 dict,然后这个继承类提供了__missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。

class StrKeyDict0(dict):  # StrKeyDict0 继承了 dict。
    def __missing__(self, key):
        if isinstance(key, str):  # 如果找不到的键本身就是字符串,那就抛出 KeyError 异常。
            raise KeyError(key)
        return self[str(key)]  # 如果找不到的键不是字符串,那么把它转换成字符串再进行查找。

    def get(self, key, default=None):
        try:
            return self[key]  # get 方法把查找工作用 self[key] 的形式委托给 __getitem__,这样在宣布查找失败 之前,还能通过 __missing__ 再给某个键一个机会。
        except KeyError:
            return default  # 如果抛出 KeyError,那么说明 __missing__ 也失败了,于是返回 default。

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()
        # 先按照传入键的原本的值来查找(我们的映射类型中可能含有非字符串的键),如果 没找到,再用 str() 方法把键转换成字符串再查找一次。

为什么 isinstance(key, str) 测试在上面的 __missing__ 中是必需的?
如果没有这个测试,只要 str(k) 返回的是一个存在的键,那么 __missing__ 方法是没问题的,不管是字符串键还是非字符串键,它都能正常运行。但是如果 str(k) 不是一个存在的键,代码就会陷入无限递归。这是因为 __missing__ 的最后一行中的self[str(key)] 会调用 __getitem__,而这个 str(key) 又不存在,于是__missing__ 又会被调用。
为了保持一致性,__contains__ 方法在这里也是必需的。这是因为 k in d 这个操作会调用它,但是我们从 dict 继承到的 __contains__ 方法不会在找不到键的时候调用__missing__ 方法。__contains__ 里还有个细节,就是我们这里没有用更具 Python 风格的方式——k in my_dict——来检查键是否存在,因为那也会导致 __contains__ 被递归调用。为了避免这一情况,这里采取了更显式的方法,直接在这个 self.keys() 里查询。
出于对准确度的考虑,我们也需要这个按照键的原本的值来查找的操作(也就是 key in self.keys()),因为在创建 StrKeyDict0 和为它添加新值的时候,我们并没有强制要求传入的键必须是字符串。因为这个操作没有规定死键的类型,所以让查找操作变得更加友好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值