瓜皮学Python之字典和集合

本文详细介绍了Python中的字典和集合操作,包括字典的现代语法如推导式、映射解包和模式匹配,以及集合的运算和字典视图。文中强调了字典的高效性和哈希表的底层实现,同时提到了Python 3.9新增的合并映射运算符`|`。文章还探讨了如何处理不存在的键,如使用`defaultdict`和`__missing__`方法,并介绍了标准库中的`OrderedDict`、`Counter`和`Shelf`等映射类型。此外,文章还讨论了集合的字面量、推导式和集合运算,以及字典视图作为集合的类似行为。
摘要由CSDN通过智能技术生成

在所有的Python程序中都会使用到字典。即便没在代码中直接使用,也是间接用到,因为dict类型是Python实现的一个基础。类和实例发不发、模块命名空间以及函数关键词参数都是在内存以及字典表示的核心Python结构。__builtins__.__dict__存储着所有的内置类型、对象和函数。

因其作用重大,Python的字典进行了高度的优化,并且还在不断改进。Python高性能字典背后的引擎是哈希表。

其它基于哈希表的内置类型有setfrozenset。它们提供了比其它流行编程语言中集合更多的API和运算符。具体来说,Python集合实现了集合理论中的所有基础运算,如并集、交集、子集测试等。借助于它们,我们以更为声明式的方式表达算法,避免大量的嵌套循环和条件语句。

以下本章的概述:

  • 构建、处理dicts和映射的现代语法,包括增强的解包和模式匹配。
  • 映射类型的常用方法。
  • 缺失键的特殊处理。
  • 标准库中dict的变体。
  • setfrozenset类型。
  • 集合和字典行为中哈希表的内涵。

现代字典语法

下面的小节中讲解构建、解包和处理映射的高级语法。其中一些特性并不是语言中新增的,但对于读者而言可能第一次听到。有一部分语法要求使用Python 3.9(比如管道运算符 |) 或Python 3.10(如 match/case)。我们先从一个优秀而又古老的特性开始。

dict推导式

从Python 2.7开始,列表推导式和生成式表达式就进行了dict推导式(以及set推导式,稍后会讲解)的适配。字典推导式通过从任意迭代对象接收key:value对构造一个dict实例。例3-1演示了使用dict推导式通过同一个元组列表构造两个字典。

例3-1:字典推导式的示例

>>> dial_codes = [           # dial_codes键值对可迭代对象可直接传递给dict构造函数,但是...                                                
...     (880, 'Bangladesh'),
...     (55,  'Brazil'),
...     (86,  'China'),
...     (91,  'India'),
...     (62,  'Indonesia'),
...     (81,  'Japan'),
...     (234, 'Nigeria'),
...     (92,  'Pakistan'),
...     (7,   'Russia'),
...     (1,   'United States'),
... ]
>>> country_dial = {country: code for code, country in dial_codes}  # ...此处进行键值互调:country为键,code为值
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62,
'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
>>> {code: country.upper()   # 按名称对country_dial排序,键值互调,值置为大写,然后使用code < 70进行过滤
...     for country, code in sorted(country_dial.items())
...     if code < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}
复制代码

如果会使用列表推导式的话,就自然会使用字典推导式。如若不会,推导式语法的广泛传播表明流畅使用它能带来诸多好处。

映射解包

PEP 448—解包综述增补自Python 3.5开始增强了对两种映射解包的支持。

首先可在函数调用对一个以上的参数使用**。在所有参数的键为字符串且唯一时可进行使用(因为允许使用重复的关键字参数)。

>>> def dump(**kwargs):
...     return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
复制代码

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

>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}
复制代码

上例中出现了重复的键,这是允许的。后出现的会重写先出现的,可以看下本例中x的值。

这种语法也可用于合并映射,但还有其它的方式。请听后文分解。

使用管道符|合并映射

Python 3.9中支持使用||=合并映射。这很符合逻辑,国类这两者同时也是集合并集运算符。

使用|运算符新建映射:

>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}
复制代码

译者注:映射(mapping)是一种由键和关联值的集合组成的数据类型。目前 Python 唯一内置的映射类型是字典。

本文中会用到 Python 3.9和 Python3.10,当然可以安装最新版,或是在本地安装多个版本的Python,但也可以通过 Docker 进行测试(通过指定镜像版本就可以使用对应版本的 Python,以下默认使用最新版)

docker run -d --name python-alpine python:alpine watch "date >> /var/log/date.log"
docker exec -it python-alpine sh
# 用完就退出也可使用
docker run -it python:alpine sh
复制代码

通常新映射的类型与左项的类型一致,上例为d1,但如果存在用户自定义类型则亦可为第二项的类型,在第16章中的运算符重载规则部分会进行讲解。

就地更新已有映射可使用|=。继续对前例进行操作,上例中的d1未发生修改,但下例中则不然:

>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}
复制代码

小贴士:如需维护Python 3.8或更早版本中运行的代码,PEP 584—添加并集运算符动机一节中总结了几种合并映射类型的方法。

下面来学习映射的模式匹配。

映射的模式匹配

match/case语句支持映射对象主体。映射的模式类似于字典字面量,但可以匹配任意实例或collections.abc.Mapping的虚拟子类。

第2章中,我们只讨论了序列的模式,但不同的类型的模式可进行合并、内嵌。借助于解构,模式匹配是一种处理映射和序列嵌套之类结构记录的强大工具,通常用于读取JSON API和半结构化模式(schema)数据库,如MongoDB、EdgeDB或PostgreSQL。例3-2中进行了演示。get_creators中的简单类型提示表明接收了一个字典,返回了一个列表。

例3-2:creator.py: get_creators()从媒体记录中提取创作者名称

def get_creators(record: dict) -> list:
    match record:
        case {'type': 'book', 'api': 2, 'authors': [*names]}:  # 匹配任意带'type': 'book', 'api' :2以及映射序列的'authors'键映射。以新的列表进行返回
            return names
        case {'type': 'book', 'api': 1, 'author': name}:  # 匹配任意带'type': 'book', 'api' :2以及映射对象的'authors'键映射。在列表内部返回对象。
            return [name]
        case {'type': 'book'}:  # 其它带'type': 'book的映射均无效,抛出ValueError
            raise ValueError(f"Invalid 'book' record: {record!r}")
        case {'type': 'movie', 'director': name}:  # 匹配任意带'type': 'movie', 'api' :2以及映射单个对象的'director'键映射。在列表内部返回对象
            return [name]
        case _:  # 其它均为无效,抛出ValueError
            raise ValueError(f'Invalid record: {record!r}')
复制代码

例3-2很好地演示了在处理JSON之类的半结构化数据:

  • 包含一个描述记录类型的字段(如'type': 'movie'
  • 包含一个标识模式版本的字段(如'api': 2'),方便未来仅有API的演进
  • case从句处理具体类型的无效记录(如'book'),以及异常捕获

下面我们来看get_creators是如何处理具体的文档测试的:

>>> b1 = dict(api=1, author='Douglas Hofstadter',
...         type='book', title='Gödel, Escher, Bach')
>>> get_creators(b1)
['Douglas Hofstadter']
>>> from collections import OrderedDict
>>> b2 = OrderedDict(api=2, type='book',
...         title='Python in a Nutshell',
...         authors='Martelli Ravenscroft Holden'.split())
>>> get_creators(b2)
['Martelli', 'Ravenscroft', 'Holden']
>>> get_creators({'type': 'book', 'pages': 770})
Traceback (most recent call last):
    ...
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
>>> get_creators('Spam, spam, spam')
Traceback (most recent call last):
    ...
ValueError: Invalid record: 'Spam, spam, spam'
复制代码

注意模式中键的排序不重要,像b2中的的有序字典也一样没关系。

不同于序列模式,映射只需要部分匹配即可成功。在文档测试中b1b2包含'title'键在所有的'book'模式中都没有,但仍能匹配成功。

无需使用**extra来匹配其它的键值对,但如果希望以字典来捕获,可以在一个变量名前添加**。这个变量必须是模式中最后的那个,不允许使用**_,因为这有点画蛇添足。举个简单的例子:

>>> 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}
复制代码

自动处理无键返回值一节我们会学习defaultdict和其它通过__getitem__(即 d[key]) 来查询键的映射,因其会实时创建缺失项,所以执行成功。在模式匹配中,只有在match语句所需要的键存在时匹配才会成功。

小贴士:没有触发对缺失键的自动处理,原因是模式匹配总会使用d.get(key, sentinel)方法,其中默认的sentinel是一个无法在用户数据中出现的特殊标记值。

语法和结构暂时讲到这,下面学习映射的API。

映射类型的标准API

collections.abc模块中提供了MappingMutableMapping抽象基类,描述dict和类似类型的接口。参见图3-1

抽象基础类的主要价值是记录及统一映射的标准接口,并对需要支持映射的类型在代码中进行isinstance测试时作为其条件:

>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True
复制代码

小贴士:使用抽象基类的isinstance通常比检测函数参数是否为dict类型要好,因为这样可以使用其它映射类型。我们会在第13章中详细讨论

图3-1:collections.abc中MutableMapping及其父类的简化UML类图(继承箭头由子类指向父类,斜体名称为抽象类和抽象方法)

要实现自定义映射,继承collections.UserDict或通过组合封装dict会比抽象基类的这些子类更容易。collections.UserDict类和所有标准库中的所有具体映射类在实现时封装了基础的dict,然后根据哈希表构建。因此,这些类型的键都必须为可哈希对象(值并无此求,仅针对键)。如果需要复习可哈希的概念,下一节中会进行讲解。

什么是可哈希对象

下面是Python词汇表中节略的对可哈希对象的定义。

一个对象可哈希的意思是在其生命周期内哈希码都不发生改变(使用__hash__()方法),并可与其它对象进行比较(使用__eq__()方法)。相等的可哈希对象必须要有一致的哈希码。

数据类型和普通的不可变类型strbytes都是可哈希对象。容器类型在不可变及的所含对象也均不可变时是可哈希的。frozenset一定是可哈希的,因为其所含的每个元素在定义上都必须是可哈希的。tuple仅在所有元素都可哈希时才可哈希。参见元组tttltf

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
-3907003130834322577
>>> tl &#
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值