流畅的python,Fluent Python 第三章笔记
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)
<ipython-input-538-495985ac56c1> in <module>
----> 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)
<ipython-input-590-248d64c43459> in <module>
----> 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 <container_type>' % 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是不要用吗?
浙公网安备 33010602011771号