3.1 泛映射类型
collections.abc模块中有Mapping和MutableMapping这两个抽象基类,它们的作用是为dict和其他类似的类型定义形式接口。
非抽象映射类型一般不会直接继承这些抽象基类,它们会直接对dict或是collections.UserDict进行扩展。这些抽象基类的主要作用是作为形式化的文档,它们定义了构建一个映射类型所需的最基本的接口。它们还可以跟isinstance一起被用来判定某个数据是不是广义的映射类型:
>>> from collections import abc >>> my_dict = {} >>> isinstance(my_dict,abc.Mapping) True
这里用isinstance而不是type来检查某个参数是否为dict类型,因为这个参数有可能不是dict,而是一个比较另类的映射类型。
原子不可变数据类型(str、bytes和数值类型)都是可散列类型,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) 985328935373711578
创建字典的不同方法:
>>> 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 字典的推导
字典推导可以从任何以键值对作为元素的可迭代对象中构建出字典。
例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} >>> country_code {'China': 86, 'India': 91, 'United States': 1, 'Indonesia': 62, 'Brazil': 55, 'P akistan': 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 常见的映射方法
default_factory并不是一个方法,而是一个可调用对象(callable),它的值在defaultdict初始化的时候由用户设定。
OrderDict.popitem()会移除字典里最先插入的元素(先进先出);同时这个方法还有一个可选的last参数,若为真,则会移除最后插入的元素(后进先出)。
用setfault处理找不到的键
例2,index0.py这段程序从索引中获取单词出现的频率信息,并把它们写进对应的列表里
"""创建一个从单词到其出现情况的映射""" 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) # 这其实是一种很不好的实现,这样写只是为了证明论点 occurences = index.get(word,[]) occurences.append(location) index[word] = occurences # 以字母顺序打印出结果 for word in sorted(index,key=str.upper): print(word,index[word])
例3,这里是示例2的不完全输出。每一行的列表都代表一个单词的出现情况,列表中的元素是一对值,第一个值表示出现的行,第二个表示出现的列。
例4,index.py用一行就解决了获取和更新单词的出现情况列表,当然跟示例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 映射的弹性键查询
3.4.1 defaultdict:处理找不到的键的一个选择
例5,index_default.py:利用defaultdict实例而不是setdefault方法
"""创建一个从单词到其出现情况的映射""" import sys import re import collections WORD_RE = re.compile(r'w+') #index = { } # 把list构造方法作为defaul_factor来创建一个defaultdict 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])
3.4.2 特殊方法_missing_
_misssing_方法只会被_getitem_调用。defaultdict中的default_factor只对_getitem_有作用的原因。
例6,当有非字符串的键被查找的时候,StrKeyDict0是如何在该键不存在的情况下,把它转移为字符串的
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() d = StrKeyDict0([('2','two'),('4','four')]) print('d[''2'']:'+d['2']) print('d[4]:'+d[4]) print('d[1]:'+d[1])
d[2]:two Traceback (most recent call last): d[4]:four File "D:/PycharmProject/Study/strkeydict0.py", line 17, in <module> print('d[1]:'+d[1]) File "D:/PycharmProject/Study/strkeydict0.py", line 5, in __missing__ return self[str(key)] File "D:/PycharmProject/Study/strkeydict0.py", line 4, in __missing__ raise KeyError(key) KeyError: '1'
print(d.get('2')) print(d.get(4)) print(d.get(1,'N/A')) print(2 in d) print(1 in d) two four N/A True False
3.5 字典的变种
collections.OrderedDict
这个类型在添加键的时候回保持顺序,因此键的迭代次序总是一致的。OrderedDict的popitem方法默认删除并返回的是字典里的最后一个元素,但是如果像my_odict.popitem(last=False)这样调用它,那么它删除并返回第一个元素被添加进去的元素。
collections.ChainMap
该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当做一个整体被逐个查找,知道键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。
Python变量查询规则的代码片段:
import builtins
pylookup = ChainMap(locals(),globals(),vars(builtins))
collections.Counter
这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。
例7,利用Counter来计算单词中各个字母出现的次数:
>>> import collections >>> ct = collections.Counter('aevrvgve') >>> ct Counter({'v': 3, 'e': 2, 'a': 1, 'r': 1, 'g': 1}) >>> ct.update('aaaaazzzz') >>> ct Counter({'a': 6, 'z': 4, 'v': 3, 'e': 2, 'r': 1, 'g': 1}) >>> ct.most_common(2) [('a', 6), ('z', 4)]
collections.UserDict
这个类其实就是把标准dict用纯python又实现了一遍。和上面所说的开箱即用的类型不一样,UserDict是让用户继承写子类的。
3.6 子类化UserDict
UserDict并不是dict的子类,但是UserDict有一个叫作data的属性,是dict的实例,这个属性实际上是UserDict最终存储数据的地方。
例8,无论是添加、更新还是查询操作,StrKeyDict都会把非字符串的键转换为字符串
import collections class StrKeyDict(collections.UserDict): def __missing__(self, key): if isinstance(key,str): raise KeyError(key) return self[str(key)] # 这里放心假设所有已经存储的键都是字符串,因此只要在self.data上查询就好了 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这些超类继承而来的。
以下两个方法都值得关注:
MutableMapping.update
这个方法不但可以为我们直接利用,它还用在__init__里,让构造方法可以利用传入的各种参数(其他映射类型、元素是(key,value)对的可迭代对象和键值参数)来新建实例。因为这个方法在背后是用self[key] = value来添加新值的,所以它其实是在使用我们的__setitem__方法。
Mapping.get
在例6中,不得不改写get方法,好让它的表现跟__getitem__一致。而在例8中就没有必要了,因为它继承了Mapping.get方法。
3.7 不可变映射类型
标准库例所有的映射类型都是可变的。从Python3.3开始,types模块中引入了一个封装类名叫MappingProxyType。如果这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是他是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。
例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 "<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'
1、d的内容可以通过d_proxy看到。
2、但是通过d_proxy不能做任何修改。
3、d_proxy是动态的,也就是说对d所做的任何改动都会反馈到它上面。
3.8 集合论
set(可变集合)与frozenset(不可变集合)的区别:
set无序排序且不重复,是可变的,有add(),remove()等方法。既然是可变的,所以它不存在哈希值。基本功能包括关系测试和消除重复元素. 集合对象还支持union(联合), intersection(交集), difference(差集)和sysmmetric difference(对称差集)等数学运算.
sets 支持 x in set, len(set),和 for x in set。作为一个无序的集合,sets不记录元素位置或者插入点。因此,sets不支持 indexing, 或其它类序列的操作。
frozenset是冻结的集合,它是不可变的,存在哈希值,好处是它可以作为字典的key,也可以作为其它集合的元素。缺点是一旦创建便不能更改,没有add,remove方法。
集合的本质是许多唯一对象的聚集。因此,集合可以用于去重:
>>> l = ['spam','spam','eggs','spam'] >>> set(l) {'eggs', 'spam'} >>> list(set(l)) ['eggs', 'spam']
集合中的元素必须是可散列的,set类型本身是不可散列的,但是frozenset可以。
例10,needles的元素在haystack里出现的次数,两个变量都是set类型
fountd = len(needles & haystack)
如果不用交集操作:
例11,needles的元素在haystack里出现的次数(作用与例10相同)
found = 0
for n in needles:
if n in haystack:
found +=1
例10明显比例11的速度要快些;另一方面,例11可以用在任何迭代对象needles和haystack上,而例10则要求两个对象都是集合。
例12,needles的元素在haystack里出现的次数,这次的代码可以用在任何可迭代对象上
found = len(set(needles) & (haystack))
#另一种写法
found = len(set(needles).intersection(haystack))
3.8.1 集合字面量
如果要创建一个空集,你必须用不带任何参数的构造方法set()。如果只是写成了{}的形式,跟以前一样,你创建的实际是个空字典。
>>> s = {1} >>> type(s) <class 'set'> >>> s {1} >>> s.pop() 1 >>> s 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 #特殊字节码BUILD_SET几乎完成了所有的工作 4 RETURN_VALUE >>> dis('set([1])') 1 0 LOAD_NAME 0 (set) 2 LOAD_CONST 0 (1) #3种不同的操作代替了上面的BUILD_SET 4 BUILD_LIST 1 6 CALL_FUNCTION 1 8 RETURN_VALUE >>>
由于Python里没有针对frozenset的特殊字面量句法,我们只能采用构造法。Python3里frozenset的标准字符串形式看起来就像构造方法调用一样。来看转控制台对话:
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
3.8.2 集合推导
例13,新建一个Latin-1字符集合,该集合里的每一个字符的Unicode名字里都有“SIGN”这个单词
>>> from unicodedata import name >>> {chr(i) for i in range(32,256) if 'SIGN' in name(chr(i),'')} {'¢', '¥', '%', '§', '±', '÷', '¶', '=', '£', '+', '$', '®', '>', '¬', '×', '©', '<', '#', '¤', 'µ', '°'}
3.8.3 集合的操作
3.9 dict和set的背后
3.9.1 字典中的散列表
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。在一般的数据结构教材中,散列表里的单元通常叫做表元(bucket)。在dict的散列表中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致。所有通过偏移量来读取某个表元。
3.9.2 dict的实现及其导致结果
1、键必须是可散列的
一个可散列的对象必须满足以下要求:
(1)支持hash()函数,并且通过__hash__()方法所得到的散列值不变。
(2)支持通过__eq__()方法来检测相等性。
(3)若a==b为真,则hash(a) == hash(b)也为真
所有由用户定义的对象默认是可散列的,因为它们的散列值由id()来获取,而且它们都是不相等的。
2、字典在内存上的开销巨大
3、键查询很快
4、键的次序取决于添加顺序
当往dict里添加键而又发生散列冲突时,新键可能会被安排存放到另一个位置。
例14,diacodes.py将同样的数据以不同的顺序添加到3个字典里
# 世界人口数量前10位国家 DIAL_CODES = [ (89,'China'), (91,'India'), (1,'United States'), (62,'Indonesia'), (55,'Brazil'), (92,'Pakistan'), (880,'Bangladesh'), (234,'Nigeria'), (7,'Russia'), (81,'Japan'), ] # 创建d1的时候,数据元组的顺序是按照国家的人口排名决定的 d1 = dict(DIAL_CODES) print('d1:',d1.keys()) # 创建d2的时候,数据元组的顺序是按照国家的电话区号来决定的 d2 = dict(sorted(DIAL_CODES)) print('d2:',d2.keys()) # 创建d3的时候,数据元组的顺序是按照国家名字的英文拼写来决定的 d3 = dict(sorted(DIAL_CODES,key=lambda x:x[1])) print('d3:',d3.keys()) # 这些字典都是相等的,他们所包含的数据是一样的 assert d1 == d2 and d2 == d3
输出:
d1: dict_keys([89, 91, 1, 62, 55, 92, 880, 234, 7, 81]) d2: dict_keys([1, 7, 55, 62, 81, 89, 91, 92, 234, 880]) d3: dict_keys([880, 55, 89, 91, 62, 81, 234, 92, 7, 1])
5、往字典里添加新键可能会改变自己已有键的顺序
无论何时往字典里添加新的键,Python解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新键一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程可能会发生新的散列表冲突,导致新散列表中键的次序变化。
由此可知,不要对字典同时进行迭代和修改。如果想要扫描并修改一个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。
在Python3中,.key()、.item()和.values()方法返回的都是字典视图。也就是说,这些方法返回的值更像集合。视图有动态的特性,可以实时反馈字典的变化。
3.9.3 set的实现以及导致的结果
set和frozenset的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。
特点:
- 集合里的元素必须是可散列的。
- 集合很消耗内存。
- 可以很高效地判断元素是否存在于某个集合。
- 元素的次序取决于被添加到集合里的次序。
- 往集合里添加元素,可能改变集合里已有元素的次序。