3.1泛映射类型。
我们用的dict属于MutableMapping的子类,MutableMapping继承了Mapping,Mapping继承了Container,Iterable, Sizer
In [524]: isinstance([],Sized) Out[524]: True In [525]: isinstance({},collections.abc.MutableMapping) Out[525]: True In [526]: issubclass(dict, collections.abc.Mapping) Out[526]: True In [527]: dict.__mro__ Out[527]: (dict, object) In [528]: issubclass(dict, collections.abc.MutableMapping) Out[528]: True In [529]: issubclass(collections.abc.MutableMapping, collections.abc.Mapping) Out[529]: True
标准库里的所有映射都是利用dict来实现的,因此它们有个共同的限制,既只有可散列的数据类型才能用作这个映射里的键。
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()的方法。另外可散列对象还要有__eq__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
一半用户自定义的对象都是可散列的,散列值就是他们的id()函数的返回值。
最后写几种字典的创建方式熟悉下:
In [532]: a = dict(one=1, two=2,three=3) In [533]: b = dict(zip(('one','two','three'),(1,2,3))) In [534]: c = dict([('one',1),('two',2),('three',3)]) In [535]: a == b ==c Out[535]: True
3.2字典推导:
In [536]: {k:v for k,v in enumerate(range(1,10),1)} Out[536]: {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}
3.3 常见的映射方法
常用的就是dict,defaultdict,OrderedDict,后面两种是dict的变异,位于collections里面
用setdefault处理找不到的键。
是一个非常好用方法,里面两个参数,第一个填写key,如果字典里面有这个key,返回具体value,如果没有这个key,则新建这个k,v,value就是后面的第二个参数,并返回value。
就是说,不管能不能在字典里面找到value,都有返回值。
3.4映射的弹性查询
d_dict = defaultdict({'a':1, 'b':2}) --------------------------------------------------------------------------- TypeError Traceback (most recent call last)in ----> 1 d_dict = defaultdict({'a':1, 'b':2}) TypeError: first argument must be callable or None In [539]: d_dict = defaultdict(str,{'a':1, 'b':2}) In [540]: d_dict[12] Out[540]: '' In [541]: d_dict Out[541]: defaultdict(str, {'a': 1, 'b': 2, 12: ''})
再次使用,其实defaultdict第一个参数一定要是一个可调用参数,或者None,后面可以写一些普通的字典创建的语法。
读取这个对象时,有key就返回,没key就新建这个key位key,然后调用前面的函数,或者None,新建这组k,v,并返回这个函数的返回值或者None
同setdefault效果差不多,都是肯定有返回值的,底层都应该调用了__missing__。
刚刚又测试了下,setdefault第二个参数传递进去的时一个对象,好比[]列表,{}字典,''字符串等
defaultdict第一个参数为可调用的就可以比如函数(或类)list,dict,str等等,该对象只有[]取值的时候才会调用,对get取值,in无效。
3.4.2 特殊方法__missing__
这个方法前面我也记录了一下,书上写的更加详细。
__missing__也时在[]取值(也就是__getitem__)取不到的时候会调用该方法,dict默认的该方法会报错。
而且dict创建的字典,只有在__getitem__(就是[]取值的时候)取不到才会用__missing__
get和__contains__(in)这些方法没有影响
所以你如果继承dict的基础上,在get与in的时候激活__missing__就需要重写get与__contains__。
class StrKeyDict0(dict): '''这个一个通过数字能取到字符串数字key的字典''' def __missing__(self, key): if isinstance(key, str): # 判断类型是否为字符串,如果为字符串直接报错 raise KeyError(key) return self[str(key)] # 如果非字符串切换成字符串再次进行__getitem__的调用 def get(self, k, default=None): try: return self[k] # 首先调用__getitem__取值,如果取值失败进入__missing__ except KeyError: return default # 如果__missing__还是无法取值,接收KeyError错误,返回default def __contains__(self, key): return key in self.keys() or str(key) in self.keys() # 前面不能用key in self,这样又会调用__contains__会陷入死循环 dc = StrKeyDict0(zip(('1', 2, 3, '4'), 'love')) print(dc) print(1 in dc) # 本来只有字符串的key1 print(dc.get(1)) # 也可以通过1数字取值'1'的value print(dc.get(8))
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第三章/t3-4-2.py {'1': 'l', 2: 'o', 3: 'v', '4': 'e'} True l None Process finished with exit code 0
3.5字典的变种
collections.OrderedDict:有序字典,前面已经简单记录,一半用的比较少
collections.ChainMap:把不同的映射对象打包成一个整体,前面也简单介绍了
collections.Counter:统计小帮手,下面简单写一些代码便于记忆。
In [556]: l = (random.randint(10,20) for i in range(100)) In [557]: Counter(l) Out[557]: Counter({11: 8, 10: 8, 12: 9, 17: 12, 14: 12, 16: 14, 18: 11, 19: 6, 15: 10, 13: 7, 20: 3}) In [558]:
很厉害直接把一个生成器里面的元素数量,按照字典给出了形式。
In [564]: l = (random.randint(10,20) for i in range(100)) In [565]: c = Counter() In [566]: l2 = (random.randint(10,30) for i in range(100)) In [567]: c Out[567]: Counter() In [568]: c = Counter(l) In [569]: c Out[569]: Counter({13: 9, 15: 7, 14: 6, 20: 13, 17: 10, 11: 13, 10: 7, 19: 6, 16: 8, 18: 10, 12: 11}) In [570]: c.update(l2) In [571]: c Out[571]: Counter({13: 14, 15: 12, 14: 8, 20: 16, 17: 14, 11: 20, 10: 13, 19: 10, 16: 12, 18: 13, 12: 15, 30: 6, 22: 7, 23: 5, 29: 5, 24: 5, 27: 10, 21: 3, 25: 6, 26: 4, 28: 2}) In [572]:
扩展也很方便只要在up中添加可迭代对象就可以。
绝对的统计小帮手。
3.6子类化UserDict
UserDict在collections里面,这个一个专门让用用户继承写子类用的。
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 # 通过data调取dict属性 def __setitem__(self, key, value): self.data[str(key)] = value # 通过data调取dict属性,如果继承了dict,这里将无法设置,会进去死循环递归。 dc = StrKeyDict(zip(('1', 2, 3, '4'), 'love')) print(dc) print(1 in dc) # 本来只有字符串的key1 print(dc.get(1)) # 也可以通过1数字取值'1'的value print(dc.get(8))
通过继承UserDict让你写一个自己独特的字典相对来说更加方法也更加容易,不用考虑很多死循环递归的问题。
UserDict继承了MUtableMapping,Mapping类
MutableMapping.update可以让我们实例创建对象
Mapping.get直接继承过来可以让我们不用改get,因为Mapping.get会激活__getitem__,其中的逻辑跟前面写的修改get的方法里面一样。
3.7不可变映射类型
from types import MappingProxyType
其实还是蛮有意思的,对于字典数据的保护很好。
In [586]: from types import MappingProxyType In [587]: d = dict(name='sidian', age=18) In [588]: pd = MappingProxyType(d) In [589]: pd['name'] Out[589]: 'sidian' In [590]: pd['name'] = 'wusidian' --------------------------------------------------------------------------- TypeError Traceback (most recent call last)in ----> 1 pd['name'] = 'wusidian' TypeError: 'mappingproxy' object does not support item assignment In [591]: d['name'] = 'wudian' In [592]: pd Out[592]: mappingproxy({'name': 'wudian', 'age': 18}) In [593]:
通过这个可以看出,MappingProxyType实例后的对象时跟随的原对象的,这样就可以把MappingProxyType数据拿出来普通用户用,安全方便。
3.8 集合论
集合内的元素就好比是没有values的字典,里面的所有元素必须是可散列的,集合自身是不可散列的,但frozenset可以
简单的操作前面有记录不重复记录了。
set(1)比set([1])创建集合要快,集合推导基本跟列表推导一样。
集合有不少操作运算符,等用到在写吧。
3.9 dict和set的背后
通过in查找一个元素是否在容器里面,在大的数据集合下,字典跟集合的速度快的无所谓查询集的大小,列表就不行了,1000万数据的时候进很慢了。
''' 生成容器性能测试所需的数据 ''' import random import array MAX_EXPONENT = 7 HAYSTACK_LEN = 10 ** MAX_EXPONENT # 1000万 NEEDLES_LEN = 10 ** (MAX_EXPONENT - 1) # 100万 SAMPLE_LEN = HAYSTACK_LEN + NEEDLES_LEN // 2 # 一共1050万数据 needles = array.array('d') sample = {1/random.random() for i in range(SAMPLE_LEN)} # 集合生成式 创建数据 print('initial sample: %d elements' % len(sample)) # 查看数据长度 # 完整的样本,防止丢弃了重复的随机数 while len(sample) < SAMPLE_LEN: # 防止重复数据,如果有,就补充数据 sample.add(1/random.random()) print('complete sample: %d elements' % len(sample)) sample = array.array('d', sample) # 将1050万个数据放入数组中 random.shuffle(sample) # 打乱数组 not_selected = sample[:NEEDLES_LEN // 2] # 选出50万个后期没被选中的数据 print('not selected %d sample' % len(not_selected)) print(' writing not selected.arr') with open('not_selected.arr', 'wb') as fp: not_selected.tofile(fp) selected = sample[NEEDLES_LEN // 2:] # 选出1000万个被选中的数据 print('selected: %d samples' % len(selected)) print(' write selected.arr') with open('selected.arr', 'wb') as fp: selected.tofile(fp)
import sys import timeit SETUP = ''' import array selected = array.array('d') with open('selected.arr', 'rb') as fp: # 读取选中数据,根据size的不同,每次读取的数据不一样。 selected.fromfile(fp, {size}) if {container_type} is dict: # 判断输入的,根据不同的输入创建字典 haystack = dict.fromkeys(selected, 1) else: haystack = {container_type}(selected) # 生成集合或者列表 if {verbose}: print(type(haystack), end=' ') print('haystack:%10d' % len(haystack), end=' ') # 输出查寻集的数量 needles = array.array('d') with open('not_selected.arr', 'rb') as fp: # 先从未选中文件里面选出500个数组 needles.fromfile(fp,500) needles.extend(selected[::{size}//500]) # 从已经出来的selected数据中,间隔选举出来500个数据,size越大, if {verbose}: # selected数据越大,step也越大,但扩展的选中数据一直为500,总参与寻找的数组数据为1000个 print(' neddles: %10d' % len(needles), end=' ') ''' TEST = ''' found = 0 for n in needles: # 先从1000个参与查寻的迭代取出 if n in haystack: # 分别测试不用的查寻集 found +=1 if {verbose}: print(' found: %10d' % found) ''' def test(container_type, verbose): MAX_EXPONENT = 7 for n in range(3, MAX_EXPONENT + 1): size = 10**n # 最少的查询机为n=3,查询机最少1000个数据,最大为1000万个 setup = SETUP.format(container_type=container_type, size=size, verbose=verbose) # 都是一些参数的导入 test = TEST.format(verbose=verbose) tt = timeit.repeat(stmt=test, setup=setup, repeat=5, number=1) # 这个写的最漂亮,超严谨,用了timeit.repeat来多次测试 print('|{:{}d}|{:f})'.format(size, MAX_EXPONENT, min(tt))) # 然后最后出结果数据,大神用min最小的事件,我觉得avg平均更好 if __name__ == '__main__': if '-v' in sys.argv: # 就一个开关,-v是运行的时候显示细节,输出的信息更多 sys.argv.remove('-v') verbose = True else: verbose = False if len(sys.argv) !=2: print('Usage: %s' % sys.argv[0]) else: test(sys.argv[-1], verbose)
这个是书中大神写的测试代码,写的很太厉害了,我花了一个小时才看懂,做了一些标识。
shijianzhongdeMacBook-Pro:第三章 shijianzhong$ python3 container_perftest.py set | 1000|0.000087) | 10000|0.000086) | 100000|0.000105) |1000000|0.000194) |10000000|0.000259) shijianzhongdeMacBook-Pro:第三章 shijianzhong$ python3 container_perftest.py dict | 1000|0.000072) | 10000|0.000084) | 100000|0.000144) |1000000|0.000226) |10000000|0.000345) shijianzhongdeMacBook-Pro:第三章 shijianzhong$ python3 container_perftest.py list | 1000|0.008904) | 10000|0.077526) | 100000|0.828026) |1000000|8.404976) |10000000|84.174293)
从数据看出来,散列数据里面用in查寻实在太快了。
3.9.2 字典中的散列表
散列值的相等性:
import sys MAX_BITS = len(format(sys.maxsize, 'b')) # 确定位数,我是64位 print('%s-bit Python build' % (MAX_BITS + 1)) def hash_diff(o1, o2): h1 = '{:0>{}b}'.format(hash(o1), MAX_BITS) # 取出哈希值,用2进制格式化输出,右对齐,空位用0填充 # print(h1) h2 = '{:>0{}b}'.format(hash(o2), MAX_BITS) # print(h2) diff = ''.join('|' if b1 != b2 else ' ' for b1, b2 in zip(h1, h2)) # 通过zip压缩循环取值,对比是否相等 # print(diff) 相等留空,不想等划线 count = '|={}'.format(diff.count('|')) # 统计不想等的个数 width = max(len(repr(o1)), len((repr(o2))), 8) # 确定起头的宽度 # print(width) sep = '-' * (width * 2 + MAX_BITS) # 最后的过度线, # print(sep) return '{!r:{width}}=>{}\n {}{:{width}} {} \n{!r:{width}}=>{}\n{}'.format( o1, h1, ' ' * (width), diff, count, o2, h2, sep, width=width) # 这个格式化最骚,首先标题订宽度用width,接着输入原始数字o1,=>输出哈希值,第二行输出|竖线,后面输出不同的数量 # 第三排逻辑跟第一排一样,最后换行输出------线,这个太骚的格式化输出了 if __name__ == '__main__': print(hash_diff(1, 1.01)) print(hash_diff(1.0, 1)) print(hash_diff('a', 'A')) print(hash_diff('a1', 'a2')) print(hash_diff('sidian', 'sidian'))
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第三章/hashdiff.py 64-bit Python build 1 =>000000000000000000000000000000000000000000000000000000000000001 | | |||| | ||| | | |||| | ||| | | | |=23 1.01 =>000000001010001111010111000010100011110101110000101001000000001 ------------------------------------------------------------------------------- 1.0 =>000000000000000000000000000000000000000000000000000000000000001 |=0 1 =>000000000000000000000000000000000000000000000000000000000000001 ------------------------------------------------------------------------------- 'a' =>010000011000101101101100110100111001011001101100000110110101001 | | | | | ||||| || |||| ||| ||||| | | | | | ||| |=32 'A' =>-110100100001010000100010100110101110101100010010100111010010010 ------------------------------------------------------------------------------- 'a1' =>-110111110000010011101011101011010010000111110101100101110010110 || || || | | ||||| | | | |||||| | | |||||||||| |=34 'a2' =>101010011110011001100100001000101011100010000100100111000110100 ------------------------------------------------------------------------------- 'sidian'=>-110011101101011101100000101101111011100001110111100001101111110 |=0 'sidian'=>-110011101101011101100000101101111011100001110111100001101111110 ------------------------------------------------------------------------------- Process finished with exit code 0
上面是大神的写的代码,这个格式化输出,字符串的处理实在太牛逼了。
从运行可以看出来1.0跟1的哈希值是一样的,包括True,但其实这两个数字的内部结构完全不一样。
为了让散列值能够胜任散列表索引这一角色,它们必须在索引空间中尽量分散开来。这意味着在最理想的状况下,越是相似但不相等的对象,它们散列值的差别应该越大。
上面代码已经实现。
从Python3.3开始str、bytes、datatime对象的散列值计算过程中多了随机的'加盐'这一步。所以每一次同一个对象的散列值会不一样,这可以防止DOS攻击而采取的一种安全措施。
散列表算法:
书中有一份很好的图说明,我记录一下自己的理解。当寻找一个key的时候,他会先从这个key的散列值中拿出最低的几位数字当做偏移量,去表元中查找,如果没找到,就是KeyError
如果找到了,他会对比自身的散列值与表元中那key的散列值,如果相同就返回value,不同的话,继续返回,在这个key里面多取几位,然后再去散列集查找。
前面讲的找到了表元,但里面的key的散列值与自己的散列值不一样,这个叫做散列冲突。
Python可能根据散列表的大小,增加散列表,里面散列表的尾数和用于索引的位数会随之增加,这样做的目的是为了减少散列冲突。
其实在日常使用中,查找数据,散列冲突很少发生。
3.9.3 dict的实现及其导致的结果。
在字典里面所谓的key相等就是他们的哈希值相等,所以能成为key必须能被哈希,且a==b就是hash(a)==hash(b)
字典由于使用了散列表,内存开销比较大。
往字典里添加新键可能会改变已有键的顺序
无论何时往字典里添加新的键,Python解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。
这个过程中可能会发生新的散列冲突,导致新散列表中键的次数变化。要注意的是,上面提到的这些变化是否发生以及如何发生,都依赖字典背后的具体实现,因此你不能很自信地
说自己知道背后发生了什么。如果你迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环有可能会跳过一些键,甚至是跳过字典中已经有的键。
由此可知,不要对字典同时进行迭代与修改。如果想扫描并修改一个字典,最好分成两步来进行:首先对字典进行迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。
这个就说明了,在字典的迭代的过程中,以后还要新建一个临时字典,最后用update进行升级,setdeault,defaultdict是不要用吗?