第三章,字典和集合

dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在__builtins__.__dict__模块中。

在这里插入图片描述
标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的数据类型)。
字典推导

from collections.abc import Mapping, MutableMapping

my_dict = {}

print(isinstance(my_dict, Mapping))

# 字典推导
DIAL_CODES = [
    (86, 'China'),
    (91, 'India'),
    (1, 'United States'),
    (62, 'Indonesia'),
    (55, 'Brazil'),
    (92, 'Pakistan'),
    (880, 'Bangladesh'),
    (234, 'Nigeria'),
    (7, 'Russia'),
    (81, 'Japan'),
]

n_dic = {key.upper():value for value,key in DIAL_CODES if value < 66}

print(n_dic)

常见的映射方法
在这里插入图片描述
在这里插入图片描述

用setdefault处理找不到的键

  • 当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的“快速失败”哲学。也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k],给找不到的键一个默认的返回值(这比处理 KeyError 要方便不少)。但是要更新某个键对应的值的时候,不管使用 getitem 还是 get 都会不自然,而且效率低
"""创建一个从单词到其出现情况的映射"""
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)
            # 这其实是一种很不好的实现,这样写只是为了证明论点
            occurrences = index.get(word, [])
            occurrences.append(location)
            index[word] = occurrences
# 以字母顺序打印出结果
for word in sorted(index, key=str.upper):
    print(word, index[word])

# ===========================================================================

"""创建从一个单词到其出现情况的映射""" 
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])

# dict.setdefault方法是在原字典修改

映射的弹性键查询

  • 有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现__missing__ 方法。

defaultdict:处理找不到的键的一个选择

有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。有两个途径能帮我们达到这个目的,
一个是通过 defaultdict 这个类型而不是普通的 dict,
另一个是给自己定义一个 dict 的子类,然后在子类中实现__missing__ 方法。

 - defaultdict
 	collections.defaultdict 在创建的时候,需要给他配置一个找不到键创造默认值的方法,
 	在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在 __getitem__ 碰到找不到的键的时候被调用,让 __getitem__ 返回某种默认值
 	-- 比如,我们新建了这样一个字典:dd=defaultdict(list),如果键 'new-key' 在 dd中还不存在的话,表达式 dd['new-key'] 会按照以下的步骤来行事。
(1)  调用 list() 来建立一个新列表。
(2)  把这个新列表作为值,'new-key' 作为它的键,放到 dd 中。
(3) 返回这个列表的引用。

"""创建一个从单词到其出现情况的映射"""
import sys
import re
import collections
WORD_RE = re.compile(r'\w+')
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])
'''
把 list 构造方法作为 default_factory 来创建一个 defaultdict。
如果 index 并没有 word 的记录,那么 default_factory 会被调用,为查询不到的键创造一个值。
这个值在这里是一个空的列表,然后这个空列表被赋值给 index[word],继而被当作返回值返回,
因此append(location) 操作总能成功
'''
# 如果在创建 defaultdict 的时候没有指定 default_factory,查询不存在的键会触发KeyError。

'''
所有这一切背后的功臣其实是特殊方法 __missing__。它会在 defaultdict 遇到找不到的键
的时候调用 default_factory,而实际上这个特性是所有映射类型都可以选择去支持的。
'''

特殊方法__missing__

  • 所有的映射类型在处理找不到的键的时候,都会牵扯到 missing 方法。 __missing__方法只会被__getitem__调用,
# MyDict在查询的时候把非字符串的键转换为字符串

class MyDict(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()

字典的变种*

  • collections.OrderedDict
    -这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。
  • collections.ChainMap
    -该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。在 collections文档介绍 ChainMap 对 象 的 那 一 部 分(https://docs.python.org/3/library/collections.html# collections.ChainMap)里有一些具体的使用示例,其中包含了下面这个 Python 变量查询规则的代码片段:
import builtins 
pylookup = ChainMap(locals(), globals(), vars(builtins))
  • collections.Counter
    -这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。Counter 实现了 + 和 - 运算符用来合并记录,还有像 most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最常见的 n 个键和它们的计数,详情参阅文档(https://docs.python.org/3/library/collections.html#collections.Counter)。下面的小例子利用 Counter 来计算单词中各个字母出现的次数:
>>> ct = collections.Counter('abracadabra') 
>>> ct 
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) 
>>> ct.update('aaaaazzz') 
>>> ct 
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) 
>>> ct.most_common(2) 
[('a', 10), ('z', 3)]
  • colllections.UserDict
    -这个类其实就是把标准 dict 用纯 Python 又实现了一遍。

不可变映射类型

  • python3.3开始,types模块中引入了一个封装类名叫MappingProxyType 如果给这个类一个映射,它会返回一个只读的映射视图。如果对原映射做了改动,通过整个视图可以观察到。
>>> from types import MappingProxyType
>>> d = {1:'a'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({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[2] = 'c'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d_proxy[2]
'b'
>>>
MappingProxyType不可被修改

集合论

  • 集合的本质是许多唯一对象的聚集,所以可以被用来去重。
  • 集合的元素必须是可散列的,但是frozenset本身是可以散列的,所以可以创建一个包含frozenset的set。
  • 集合实现了很多基础的中缀运算符
给定集合a和b
a|b  返回的是他们的合集
a&b 返回的是他们的交集
a-b  是差集

集合字面量

  • 除空集外,集合的字面量 —{1},{1,2,3} ,如果是空集,那么必须写成set()形式,如果写成{} 则解释器认为是一个空字典。

集合推导

{i for i in range(10) if i % 2 == 0}

集合的操作
在这里插入图片描述
dict和set的背后

  • Python 里的 dict 和 set 的效率有多高?
  • 为什么它们是无序的
  • 为什么并不是所有的对象都可以当作dict的键或set里的元素。
  • 为什么dict的键和set元素的顺序是根据它们被添加的次序而定的,以及为什么在映射对象的生命周期中,整个顺序并不是一成不变的。
  • 为什么不应该在迭代循环dict或是set的同时往里添加元素。

字典中的散列表

  • 散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。散列表里的单元通常叫做表元。在dict的散列表中,每个键值对都占用一个表元,每个表元都有俩个部分,一个是对键的引用,另一个是对值的引用。因为所有的表元大小都一直。可以通过偏移量来读取某个表元。
  • 因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
  • 如果要把一个对象放入散列表,那么首先要计算这个元素键的散列表。python中可以用hash()做这个事。
  1. 内置的hash可以用于任何的内置对象。如果俩个对象在比较的时候是相等的,那么他们的散列也必须相等,例如:1==1.0 为true,那么hash(1) == hash(1.0) 也必须为true,
  2. 为了让散列值能够胜任散列表索引这一角色,他们必须在索引空间中尽量分散开来,(在理想的情况下,越是不相等的对象,他们的散列值的差别应该也越大)
  • 散列表算法
    为了获取 my_dict[search_key] 背后的值,Python 首先会调hash(search_key)计算 search_key 的散列值把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出KeyError 异常。若不是空的,则表元里会有一对found_key:found_value。这时候 Python 会检验 search_key== found_key 是否为真,如果它们相等的话,返回 found_value。
    如果 search_key 和 found_key 不匹配的话,这种情况称为散列冲突。发生这种情况是因为。散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。若这次找到的表元是空的,则同样抛出 KeyError;若非空,或者键匹配,则回这个值;或者又发现了散列冲突,则重复以上的步骤。
    在这里插入图片描述

dict的实现及其导致的结果
散列给dict带来的优势和限制

  • 键必须是可散列的
    一个可散列的对象必须满足以下需求
    1)支持hash函数,并通过__hash__()方法所得到的散列值是不可变的。
    2)支持通过__eq__()方法检测相等性。
    3)若a == b 为真,则hash(a) == hash(b) 也为真。
    所有用户自定义的对象默认都是可散列的,因为它们的散列值由id()来获取,而且它们不相等。
  • 字典在内存上的开销巨大
    因为字典运用了散列表,而散列表又必须是稀疏的,导致需要占用很大空间。
    如果需要存放巨大的记录,那么放在元组或者具名元组中比较好。
    用元组的原因:
    1)避免了散列表耗费的空间
    2)不需要把记录中字段的名字在每个元素里都存一遍。
  • 键查询很快
    dict是典型的空间换时间:
  • 建的次序取决于添加顺序
    当往dict中添加值的时候,可能发生散列冲突,新的键可能会被放到另一个位置。
  • 往字典里添加键可能会改变已有键的顺序
    无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。要注意的是,上面提到的这些变化是否会发生以及如何发生,都依赖于字典背后的具体实现,如果你在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环很有可能会跳过一些键——甚至是跳过那些字典中已经有的键。由此可知,不要对字典同时进行迭代和修改。
    如果想扫描并修改一个字典,最好分成两步来进行
    1)首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;
    2)迭代结束之后再对原有字典进行更新。

set的实现以及导致的结果

set和frozenset的实现也依赖散列表,但是它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)

  • 字典和散列的几个特点也适用于集合
    1)集合里的元素必须是可散列的
    2)集合很消耗内存
    3)可以高效的判断元素是否存在于某个集合
    4)元素的次序取决于被添加到集合里的次序。
    5)往集合里添加元素,可能会改变集合里已有的元素的次序。

总结

  • 字典算得上是python的基石,python提供了很多特殊映射类型,比如:defaultdict,OrderedDict,ChainMap和Counter,这些类型都属于collections模块。以及可以扩展的UserDict类
  • 大多数映射类型都提供了俩个很强大的方法,setdefault,和update,setdefault方法可以用来更新字典的可变值(比如列表),避免了重复的键搜索。update则是提供了批量更新。
  • __missing__当对象找不到键的时候可以通过这个方法自定义返回什么。
  • colections.abc 模块提供了Mapping 和 MutableMapping 这两个抽象基类。可以进行类型查询或者引用。
  • MappingProxyType 可以用来创建不可变映射对象,它被封装在 types 模块中。另外还有 Set 和MutableSet 这两个抽象基类
  • dict 和 set 背后的散列表效率很高,被保存的元素会呈现出不同的顺序,以及已有的元素顺序会发生变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值