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

特点:

  • 集合里的元素必须是可散列的。
  • 集合很消耗内存。
  • 可以很高效地判断元素是否存在于某个集合。
  • 元素的次序取决于被添加到集合里的次序。
  • 往集合里添加元素,可能改变集合里已有元素的次序。

猜你喜欢

转载自www.cnblogs.com/cathycheng/p/11328379.html