流畅的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)
 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是不要用吗?

你可能感兴趣的:(流畅的python,Fluent Python 第三章笔记)