Python深入-3-字典与集合

一、字典新用法

1.1 字典推导式

自 Python 2.7 开始,列表推导式和生成器表达式经过改造,以适用于字典推导式(以及后文要讲的集合推导式)。字典推导式从任何可迭代对象中获取键值对,构建 dict 实例。

#  这种包含键值对的可迭代对象可以直接传给 dict 构造函数
dial_codes = [(880'Bangladesh'), (55'Brazil'), (86'China'), (91'India'), (62'Indonesia'), (81'Japan'),
              (234'Nigeria'), (92'Pakistan'), (7'Russia'), (1'United States')]

#  字典推导式;这里 我们对调了键和值的位置,以 country 为键,以 code 为值
country_dial = {country: code for  code, country in dial_codes}
print(
    country_dial)  # {'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62, 'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}

code_up = {
    code: country.upper() for country, code in sorted(country_dial.items())
}
print(code_up) # {880: 'BANGLADESH', 55: 'BRAZIL', 86: 'CHINA', 91: 'INDIA', 62: 'INDONESIA', 81: 'JAPAN', 234: 'NIGERIA', 92: 'PAKISTAN', 7: 'RUSSIA', 1: 'UNITED STATES'}

1.2 映射拆包

首先,调用函数时,不止一个参数可以使用**。但是,所有键都要是字符串,而且在所有参数中是唯一的(因为关键字参数不可重复)。

还记得列表推导式吗,拆包时使用过的是*来接收;这里字典使用的是**

# 参数是一个字典类型的拆包,**说明参数可以是多个
def dump(**kwargs):
    return kwargs


# 传参数的时候,需要**配合字典来传递
ret = dump(**{'x'1}, y=2, **{'z'3})

print(ret)  # {'x': 1, 'y': 2, 'z': 3}

其次,** 可在 dict 字面量中使用,同样可以多次使用。

print({'a'0, **{'x'1}, 'y'2, **{'z'3'x'4}})  
# {'a': 0, 'x': 4, 'y': 2, 'z': 3}

这种情况下允许键重复,后面的键覆盖前面的键,比如本例中 x 映射的值。

1.3 使用 | 合并映射

Python 3.9 支持使用 ||= 合并映射。这不难理解,因为二者也是并集运算符。
**|**运算符创建一个新映射。

d1 = {'a'1'b'3}
d2 = {'a'2'b'4'c'6}
print(d1)  # {'a': 1, 'b': 3}
print(d2)  # {'a': 2, 'b': 4, 'c': 6}
# 结果具有并集操作
print(d1 | d2)  # {'a': 2, 'b': 4, 'c': 6}

通常,新映射的类型与左操作数(本例中的 d1)的类型相同。不过,涉及用户定义的类型时,也可能与第二个操作数的类型相同。
如果想就地更新现有映射,则使用|=。续前例,当时 d1 没有变化,但是现在变了。

d1 = {'a'1'b'3}
d2 = {'a'2'b'4'c'6}
print(d1)  # {'a': 1, 'b': 3}
print(d2)  # {'a': 2, 'b': 4, 'c': 6}
# 结果具有并集操作
print(d1 |= d2) 

二、使用模式匹配处理映射

match/case语句的匹配对象可以是映射。映射的模式看似 dict 字面量,其实能够匹配collections.abc.Mapping的任何具体子类或虚拟子类
例子:get_creators 函数有一些简单的类型注解,作用是明确表明参数为一个 dict,返回值是一个 list

def get_creators(record: dict) -> list:
    match record:
        # 匹配含有 'type': 'book', 'api' :2,而且 'authors' 键映射一个序列的映射对象。以列表形式返回序列中的项。
        case {'type''book''api'2'authors': [*names]}:
            return names

        # 匹配含有 'type': 'book', 'api' :1,而且 'authors' 键映射任何对象的映射对象。以列表形式返回匹配的对象
        case {'type''book''api'1'author': name}:
            return [name]

        # 其他含有 'type': 'book' 的映射均无效,抛出 ValueError
        case {'type''book'}:
            raise ValueError(f"Invalid 'book' record: {record!r}")

        # 匹配含有 'type': 'movie',而且 'director' 映射单个对象的映射对象。以列表形式返回匹配的对象。
        case {'type''movie''director': name}:
            return [name]

        # 其他匹配对象均无效,抛出 ValueError。
        case _:
            raise ValueError(f'Invalid record: {record!r}')


# 定义一个字典
b1 = dict(api=1, author='Douglas Hofstadter', type='book', title='Gödel, Escher, Bach')
print(get_creators(b1))  # ['Douglas Hofstadter']

# from语句写在这里,目的是为了演示
from collections import OrderedDict

b2 = OrderedDict(api=2, type='book', title='Python in a Nutshell', authors='Martelli Ravenscroft Holden'.split())
print(get_creators(b2))  # ['Martelli', 'Ravenscroft', 'Holden']

print(get_creators({'type''book''pages'770}))  # ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}

注意,模式中键的顺序无关紧要。即使 b2 是一个 OrderedDict,也能作为匹配对象。
与序列模式不同,就算只有部分匹配,映射模式也算成功匹配。在上述代码中,b1 和 b2 两个匹配对象中都有 ‘title’ 键,尽管任何 ‘book’ 模式中都没有这个键,但依然可以匹配。
没有必要使用 **extra 匹配多出的键值对,倘若你想把多出的键值对捕获到一个 dict 中,可以在一个变量前面加上 **,不过必须放在模式最后。**_ 是无效的,纯属画蛇添足。下面是一个简单的例子。

food = dict(category='ice cream', flavor='vanilla', cost=199)
match food:
    case {'category''ice cream', **details}:
        print(f'Ice cream details: {details}')  # Ice cream details: {'flavor': 'vanilla', 'cost': 199}

三、映射类型的标准 API

collections.abc 模块中的抽象基类 MappingMutableMapping 描述 dict 和类似类型的接口,如图

Python深入-3-字典与集合
image.png


这两个抽象基类的主要作用是确立映射对象的标准接口,并在需要广义上的映射对象时为 isinstance 提供测试标准。

my_dict = {}
print(isinstance(my_dict, abc.Mapping))  # True
print(isinstance(my_dict, abc.MutableMapping))  # True

如果想自定义映射类型,扩展collections.UserDict 或通过组合模式包装 dict 更简单,那就不要定义这些抽象基类的子类。collections.UserDict 类和标准库中的所有具体映射类都在实现中封装了基本的 dict,而 dict 又建立在哈希表之上。因此,这些类有一个共同的限制,即键必须可哈希(值不要求可哈希,只有键需要)

3.1 `可哈希`指什么

如果一个对象的哈希码在整个生命周期内永不改变(依托__hash__() 方法),而且可与其他对象比较(依托__eq__() 方法),那么这个对象就是可哈希的。两个可哈希对象仅当哈希码相同时相等。

  • 数值类型以及不可变的扁平类型strbytes均是可哈希的。

  • 如果容器类型是不可变的,而且所含的对象全是可哈希的,那么容器类型自身也是可哈希的。

  • frozenset 对象全部是可哈希的,因为按照定义,每一个元素都必须是可哈希的。

  • 仅当所有项均可哈希,tuple 对象才是可哈希的。

# 因为元组中的每个元素,都是不可变的,所以是可hash的
tt = (12, (3040))
print(hash(tt))  # -3907003130834322577

# 而这个元组中的有一个可变的列表元素,此时hash就会报错
tl = (12, [3040])
print(hash(tl))  # TypeError: unhashable type: 'list'

# frozenset把列表变成了不可变的,那就是可hash的
tf = (12, frozenset([3040]))
print(hash(tf))  # 5149391500123939311

默认情况下,用户定义的类型是可哈希的,因为自定义类型的哈希码取自id(),而且继承自object 类的__eq__()方法只不过是比较对象ID。如果自己实现了__eq__() 方法,根据对象的内部状态进行比较,那么仅当__hash__() 方法始终返回同一个哈希码时,对象才是可哈希的。实践中,这要求 __eq__()__hash__() 只考虑在对象的生命周期内始终不变的实例属性。

3.2 常用映射方法概述

下图列出了dict,以及collections模块中两个常用变体defaultdictOrderedDict实现的方法。简单起见,省略了 object 实现的方法,[...]表示可选参数

Python深入-3-字典与集合
image.png


Python深入-3-字典与集合
image.png

3.3 插入或更新可变的值

根据 Python 的“快速失败”原则,当键 k 不存在时,d[k] 抛出错误。深谙 Python 的人知道,如果觉得默认值比抛出KeyError更好,那么可以把d[k]换成d.get(k, default)。然而,如果你想更新得到的可变值,那么还有更好的方法。

3.4 自动处理缺失的键

有时搜索的键不一定存在,为了以防万一,可以人为设置一个值,以方便某些情况的处理。人为设置的值主要有两种方法:

  • 第一种是把普通的 dict 换成 defaultdict;

  • 第二种是定义 dict 或其他映射类型的子类,实现missing方法。下面分别介绍这两种情况。

defaultdict:处理缺失键的另一种选择

对于 collections.defaultdict,d[k] 句法找不到搜索的键时,使用指定的默认值创建对应的项。

`missing` 方法

映射处理缺失键的底层逻辑在__missing__方法中。dict 基类本身没有定义这个方法,但是如果dict的子类定义了这个方法,那么dict.__getitem__找不到键时将调用__missing__方法,不抛出KeyError。

四、dict 的变体

4.1 collections.OrderedDict

自 Python 3.6 起,内置的 dict 也保留键的顺序。使用 OrderedDict 最主要的原因是编写与早期 Python版本兼容的代码。不过,dict 和 OrderedDict 之间还有一些差异,Python 文档中有说明,摘录如下 (根据日常使用频率,顺序有调整)。

  • OrderedDict 的等值检查考虑顺序。

  • OrderedDict 的popitem()方法签名不同,可通过一个可选参数指定移除哪一项。

  • OrderedDict 多了一个move_to_end()方法,便于把元素的位置移到某一端。

  • 常规的 dict 主要用于执行映射操作,插入顺序是次要的。

  • OrderedDict 的目的是方便执行重新排序操作,空间利用率、迭代速度和更新操作的性能是次要的。

  • 从算法上看,OrderedDict 处理频繁重新排序操作的效果比 dict 好,因此适合用于跟踪近期存取情况(例如在 LRU 缓存中)。

4.2 collections.ChainMap

ChainMap 实例存放一组映射,可作为一个整体来搜索。查找操作按照输入映射在构造函数调用中出现的顺序·执行,一旦在某个映射中找到指定的键,旋即结束。例如:

d1 = dict(a=1, b=3)
d2 = dict(a=2, b=4, c=6)
from collections import ChainMap

chain = ChainMap(d1, d2)
print(chain['a'])  # 1
print(chain['c'])  # 6

ChainMap 实例不复制输入映射,而是存放映射的引用。ChainMap 的更新或插入操作只影响第一个输入映射。续前例:

d1 = dict(a=1, b=3)
d2 = dict(a=2, b=4, c=6)
from collections import ChainMap

chain = ChainMap(d1, d2)
print(chain['a'])  # 1
print(chain['c'])  # 6

chain['c'] = -1
print(d1)  # {'a': 1, 'b': 3, 'c': -1}
print(d2)  # {'a': 2, 'b': 4, 'c': 6}

4.2 collections.Counter

这是一种对键计数的映射。更新现有的键,计数随之增加。可用于统计可哈希对象的实例数量,或者作为多重集(multiset)使用。Counter 实现了组合计数的+-运算符,以及其他一些有用的方法,例如most_common([n])。该方法返回一个有序元组列表,对应前 n 个计数值最大的项及其数
量。下面使用 Counter 统计词中的字母数量。

import collections

ct = collections.Counter('abracadabra')
print(ct)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

ct.update('aaaaazzz')
print(ct)  # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

print(ct.most_common(3)) # [('a', 10), ('z', 3), ('b', 2)]

注意,’b’ 和 ‘r’ 两个键并列第三,但是 ct.most_common(3) 只显示 3 项。
若想把 collections.Counter 当作多重集使用,假设各个键是集合中的元素,计数值则是元素在集合中出现的次数。

4.4 shelve.Shelf

标准库中的 shelve 模块持久存储字符串键与(以 pickle 二进制格式序列化的)Python 对象之间的映射。
模块级函数shelve.open返回一个shelve.Shelf实例,这是一个简单的键值 DBM 数据库,背后是dbm 模块。shelve.Shelf 具有以下特征。

  • shelve.Shelf 是 abc.MutableMapping 的子类,提供了我们预期的映射类型基本方法。

  • 此外,shelve.Shelf 还提供了一些其他 I/O 管理方法,例如 sync 和 close。

  • Shelf 实例是上下文管理器,因此可以使用 with 块确保在使用后关闭。

  • 为键分配新值后即保存键和值。

  • 键必须是字符串。

  • 值必须是 pickle 模块可以序列化的对象。

4.5 子类应继承 UserDict 而不是 dict

创建新的映射类型,最好扩展collections.UserDict,而不是dict
子类最好继承 UserDict 的主要原因是,内置的 dict 在实现上走了一些捷径,如果继承 dict,那就不得不覆盖一些方法,而继承 UserDict 则没有这些问题。
注意,UserDict 没有继承 dict,使用的是组合模式:内部有一个 dict 实例,名为 data,存放具体的项。这样做可以避免 __setitem__ 等特殊方法意外递归,还能简化 __contains__ 的实现。

# StrKeyDict 扩展 UserDict。
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

    # __setitem__ 把 key 转换成 str 类型。委托给 self.data 属性之后,这个方法更易于重写。
    def __setitem__(self, key, item):
        self.data[str(key)] = item

五、不可变映射

标准库提供的映射类型都是可变的,不过有时也需要防止用户意外更改映射。
types 模块提供的 MappingProxyType 是一个包装类,把传入的映射包装成一个 mappingproxy 实例,这是原映射的动态代理,只可读取。这意味着,对原映射的更新将体现在 mappingproxy 实例身上, 但是不能通过 mappingproxy 实例更改映射。

from types import MappingProxyType

d = {1'A'}
d_proxy = MappingProxyType(d)

# d_proxy 可以访问 d 中的项。
print(d_proxy)  # {1: 'A'}

print(d_proxy[1])  # A

# 不能通过 d_proxy 更改映射
d_proxy[2] = 'x'  # TypeError: 'mappingproxy' object does not support item assignment

# d_proxy 是动态的,能够反映 d 的变化。
d[2] = "B"
print(d_proxy[2])  # B

六、字典视图

dict 的实例方法.keys().values().items() 分别返回dict_keysdict_valuesdict_items  类的实例。这些字典视图是 dict 内部实现使用的数据结构的只读投影。Python 2 中对应的方法返回列表,重复 dict 中已有的数据,有一定的内存开销。另外,视图还取代了返回迭代器的旧方法。

d = dict(a=10, b=20, c=30)
values = d.values()

# 通过视图对象的字符串表示形式查看视图的内容。
print(values)  # dict_values([10, 20, 30])

# 可以查询视图的长度。
print(len(values))  # 3

# 视图是可迭代对象,方便构建列表
print(list(values))  # [10, 20, 30]

# 视图实现了 __reversed__ 方法,返回一个自定义迭代器
print(reversed(values))  # <dict_reversevalueiterator object at 0x00000166540199E0>

#  不能使用 [] 获取视图中的项
print(values[0])  # TypeError: 'dict_values' object is not subscriptable

视图对象是动态代理。更新原 dict 对象后,现有视图立即就能看到变化。

d['z'] = 99
print(d)  # {'a': 10, 'b': 20, 'c': 30, 'z': 99
print(values)  # dict_values([10, 20, 30, 99])

dict_values 类是最简单的字典视图,只实现了__len____iter____reversed__ 这 3 个特殊方法。除此之外,dict_keys 和 dict_items 还实现了多个集合方法,基本与 frozenset 类相当。

七、集合

集合,一词指代 set 和 frozenset(不可变set类型)。 
集合是一组唯一的对象。集合的基本作用是去除重复项。

# 定义一个列表,这个列表中数据有重复的
list = ['spam''spam''eggs''spam''bacon''eggs']
# 把列表转换成集合,就能去重复
print(set(list)) # {'bacon', 'spam', 'eggs'}
 如果想去除重复项,同时保留每一项首次出现位置的顺序,那么现在使用普通的 dict 即可, 如下所示。  
# 定义一个列表,这个列表中数据有重复的
list = ['spam''spam''eggs''spam''bacon''eggs']

# 去重重复,且只保留第一次出现位置的顺序,利用了dict特性
print(dict.fromkeys(list).keys()) # dict_keys(['spam', 'eggs', 'bacon'])
 集合元素必须是可哈希的对象。set 类型不可哈希,因此不能构建嵌套 set 实例的 set 对象。但是 frozenset 可以哈希,所以 set 对象可以包含 frozenset 元素。 

除了强制唯一性之外,集合类型通过中缀运算符实现了许多集合运算。给定两个集合 a 和 b,a | b 计算 并集,a & b 计算交集,a - b 计算差集,a ^ b计算对称差集。

# set1集合内容多
set1 = {"hello""world""china""beijing""zhonguo""henan""shandong"}
# set2集合少
set2 = {"china""beijing""shandong""nanjing"}

# 统计 set1中的也在set2中出现过的,用交集
print(set1 & set2)  # {'shandong', 'china', 'beijing'}

# 统计,两个集合中所有的,用并集
print(set1 | set2)  # {'henan', 'nanjing', 'shandong', 'china', 'beijing', 'zhonguo', 'hello', 'world'}

# 统计,set1中有,set2没有的,用差集
print(set1 - set2)  # {'hello', 'world', 'henan', 'zhonguo'}
# 统计set2有,set1没有的,用差集
print(set2 - set1)  # {'nanjing'}

# 对称差集 被称为异或集,是集合论中的一个概念,表示两个集合中不共有的元素构成的集合
print(set1 ^ set2)  # {'world', 'nanjing', 'hello', 'zhonguo', 'henan'}
print(set2 ^ set1)  # {'world', 'nanjing', 'hello', 'henan', 'zhonguo'}

注意:上述例子中,直接使用的set集合,当然也可以是list,然后转换成set后计算。

10.1 set 字面量

set 字面量的句法与集合的数学表示法几乎一样,例如 {1}{1, 2} 等。唯有一点例外:空 set 没有字面量表示法,必须写作 set()

注意点
创建空 set,务必使用不带参数的构造函数,即 set()。倘若写成 {},则创建的是空dict

在 Python 3 中,集合的标准字符串表示形式始终使用 {...} 表示法,唯有空集例外。

s = {1}
print(type(s))  # <class 'set'>
print(s)  # {1}
print(s.pop())  # 1
print(s)  # set()
 与调用构造函数(例如 `set([123])`)相比,使用`set`的字面量句法(例如 `{123}`)不仅速度 快,而且更具可读性。调用构造函数速度慢的原因是,Python 要查找 set 名称,找出构造函数,然后构建 一个列表,最后再把列表传给构造函数。相比之下,Python 处理字面量只需要运行一个专门的 `BUILD_SET` 字节码。  
 `
frozenset` 没有字面量句法,必须调用构造函数创建。在 Python 3 中,`frozenset 的字符串表示形式类似于构造函数调用`。代码如下。  
print(frozenset(range(5)))  # frozenset({0, 1, 2, 3, 4})

10.2  集合推导式

ss = {chr(i) for i in range(32256if 'SIGN' in name(chr(i),
                                                       '')}  
# {'#', '$', '§', '=', '¤', '¶', '÷', 'µ', '©', '¢', '®', '£', '%', '+', '>', '±', '×', '<', '°', '¥', '¬'}
print(ss)

八、集合的实现方式对实践的影响

set 和 frozenset 类型都使用哈希表实现。这种设计带来了以下影响。

  • 集合元素必须是可哈希对象,必须正确实现 hash__` 和` __eq方法。

  • 成员测试效率非常高。对于一个包含数百万个元素的集合,计算元素的哈希码就可以直接定位元素, 找出元素的索引偏移量。稍微搜索几次就能找到匹配的元素,即使穷尽搜索开销也不大。

  • 与存放元素指针的低层数组相比,集合占用大量内存。尽管集合的结构更紧凑,但是一旦要搜索的元 素数量变多,搜索速度将显著下降。

  • 元素的顺序取决于插入顺序,但是顺序对集合没有什么意义,也得不到保障。如果两个元素具有相同 的哈希码,则顺序取决于哪个元素先被添加到集合中。

  • 向集合中添加元素后,现有元素的顺序可能发生变化。这是因为哈希表使用率超过三分之二后,算法 效率会有所下降,Python 可能需要移动和调整哈希表的大小,然后重新插入元素,导致元素的相对顺 序发生变化。 

8.1 集合运算

Python深入-3-字典与集合
image.png


Python深入-3-字典与集合
image.png


返回布尔类型的运算

Python深入-3-字典与集合
image.png


Python深入-3-字典与集合
image.png

九、 字典视图的集合运算

.keys().items() 这两个 dict 方法返回的视图对象与frozenset极为相似  

Python深入-3-字典与集合
image.png
Python深入-3-字典与集合
image.png
Python深入-3-字典与集合
image.png


    需要特别注意的是,dict_keysdict_items 实现了一些特殊方法,支持强大的集合运算符,包括 &(交集)、|(并集)、-(差集)和 ^(对称差集)。 
仅当 dict 中的所有值均可哈希时,dict_items 视图才可当作集合使用。倘若 dict 中有 不可哈希的值,对 dict_items 视图做集合运算将抛出 TypeError: unhashable type 'T',其 中 T 是不可哈希的类型。相反,dict_keys 视图始终可当作集合使用,因为按照其设计,所有键均可哈希。



原文始发于微信公众号(Python之家):Python深入-3-字典与集合

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198418.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!