zoukankan      html  css  js  c++  java
  • 《流畅的Python》学习笔记3(第3章:字典和集合)

    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的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。

    特点:

    • 集合里的元素必须是可散列的。
    • 集合很消耗内存。
    • 可以很高效地判断元素是否存在于某个集合。
    • 元素的次序取决于被添加到集合里的次序。
    • 往集合里添加元素,可能改变集合里已有元素的次序。
  • 相关阅读:
    [POJ1151]Atlantis
    [POJ1177]Picture
    [POJ1765]November Rain
    Adaptively handling remote atomic execution based upon contention prediction
    Webpack 2.0 的文档
    PAT乙级 1025. 反转链表 (25)
    PAT乙级 1024. 科学计数法 (20)(未通过全部测试,得分18)
    PAT乙级 1023. 组个最小数 (20)
    PAT乙级 1022. D进制的A+B (20)
    PAT乙级 1021. 个位数统计 (15)
  • 原文地址:https://www.cnblogs.com/cathycheng/p/11328379.html
Copyright © 2011-2022 走看看