• 流畅的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}}=>{}
      {}{:{width}} {} 
    {!r:{width}}=>{}
    {}'.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是不要用吗?

  • 相关阅读:
    RTMP协议Web直播点播系统EasyDSS视频平台解决无法获取指定时间快照问题
    在线教育web无插件点播平台EasyDSS在上传部分点播文件出现无法观看问题如何修复?
    RTMP协议Web直播点播服务平台EasyDSS增加获取录像指定时间的m3u8播放地址
    RTMP协议视频平台EasyDSS编译过程中Go语言异步信息处理设计与实现
    在线课堂web无插件直播点播平台EasyDSS播放指定时间段的录像报404是什么原因?
    推流直播如何通过EasyDSS推流将内网EasyGBS视频流推到公网直播间进行直播?
    RTMP协议视频平台EasyDSS开发中如何通过Go语言 gorm 框架钩子函数精简代码?
    POJ1740 A New Stone Game 博弈论基础题 男人8题
    HDU1847 博弈论 水题
    POJ 2763 Housewife Wind LCA基础题
  • 原文地址:https://www.cnblogs.com/sidianok/p/12052373.html
Copyright © 2020-2023  润新知