如何在Python3中使用集合模块

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

Python 3 有许多内置的数据结构,包括元组、字典和列表。 数据结构为我们提供了一种组织和存储数据的方法。 collections 模块帮助我们有效地填充和操作数据结构。

在本教程中,我们将介绍 集合模块 中的三个类,以帮助您使用元组、字典和列表。 我们将使用 namedtuples 创建具有命名字段的元组,使用 defaultdict 来简洁地对字典中的信息进行分组,使用 deque 来有效地将元素添加到类似列表的对象的任一侧.

在本教程中,我们将主要处理鱼的库存,当鱼被添加到虚构的水族馆或从虚构的水族馆中移除时,我们需要对其进行修改。

先决条件

为了充分利用本教程,建议您熟悉元组、字典和列表数据类型,包括它们的语法以及如何从中检索数据。 您可以查看这些教程以获取必要的背景信息:

将命名字段添加到元组

Python 元组是不可变或不可更改的有序元素序列。 元组经常用于表示列数据; 例如,CSV 文件中的行或 SQL 数据库中的行。 水族馆可能会以一系列元组的形式跟踪其鱼类库存。

单个鱼元组:

("Sammy", "shark", "tank-a")

这个元组由三个字符串元素组成。

虽然在某些方面很有用,但这个元组并没有清楚地表明它的每个字段代表什么。 实际上,元素0是名称,元素1是物种,元素2是储罐。

鱼元组字段说明:

姓名 物种 坦克
萨米 鲨鱼 坦克-a

这张表清楚地表明,元组的三个元素中的每一个都有明确的含义。

collections 模块中的 namedtuple 允许您为元组的每个元素添加显式名称,以在 Python 程序中明确这些含义。

让我们使用 namedtuple 生成一个明确命名鱼元组每个元素的类:

from collections import namedtuple

Fish = namedtuple("Fish", ["name", "species", "tank"])

from collections import namedtuple 使您的 Python 程序可以访问 namedtuple 工厂函数。 namedtuple() 函数调用返回一个绑定到名称 Fish 的类。 namedtuple() 函数有两个参数:我们的新类的期望名称 "Fish" 和命名元素列表 ["name", "species", "tank"]

我们可以使用 Fish 类来表示之前的鱼元组:

sammy = Fish("Sammy", "shark", "tank-a")

print(sammy)

如果我们运行此代码,我们将看到以下输出:

OutputFish(name='Sammy', species='shark', tank='tank-a')

sammy 使用 Fish 类实例化。 sammy 是一个包含三个明确命名的元素的元组。

sammy 的字段可以通过它们的名称或传统的元组索引来访问:

print(sammy.species)
print(sammy[1])

如果我们运行这两个 print 调用,我们将看到以下输出:

Outputshark
shark

访问 .species 返回与使用 [1] 访问 sammy 的第二个元素相同的值。

使用 collections 模块中的 namedtuple 可以使您的程序更具可读性,同时保持元组的重要属性(它们是不可变的和有序的)。

此外,namedtuple 工厂函数为 Fish 的实例添加了几个额外的方法。

使用 ._asdict() 将实例转换为字典:

print(sammy._asdict())

如果我们运行 print,您将看到如下输出:

Output{'name': 'Sammy', 'species': 'shark', 'tank': 'tank-a'}

sammy 上调用 .asdict() 返回一个字典,将三个字段名称中的每一个映射到它们对应的值。

早于 3.8 的 Python 版本可能会输出此行略有不同。 例如,您可能会看到 OrderedDict 而不是此处显示的普通字典。

注意: 在 Python 中,带有前导下划线的方法通常被认为是“私有的”。 namedtuple 提供的附加方法(如 _asdict()._make()、._replace() 等),但是, 是 public


在字典中收集数据

在 Python 字典中收集数据通常很有用。 collections模块中的defaultdict可以帮助我们快速简洁地在字典中组装信息。

defaultdict 永远不会引发 KeyError。 如果键不存在, defaultdict 只是插入并返回一个占位符值:

from collections import defaultdict

my_defaultdict = defaultdict(list)

print(my_defaultdict["missing"])

如果我们运行此代码,我们将看到如下输出:

Output[]

defaultdict 插入并返回一个占位符值,而不是抛出 KeyError。 在这种情况下,我们将占位符值指定为列表。

相比之下,常规字典会在缺少的键上抛出 KeyError

my_regular_dict = {}

my_regular_dict["missing"]

如果我们运行此代码,我们将看到如下输出:

OutputTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'missing'

当我们尝试访问不存在的键时,常规字典 my_regular_dict 会引发 KeyError

defaultdict 的行为与普通字典不同。 defaultdict 没有在缺少的键上引发 KeyError ,而是调用不带参数的占位符值来创建新对象。 在这种情况下 list() 创建一个空列表。

继续我们虚构的水族馆示例,假设我们有一个代表水族馆库存的鱼元组列表:

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]

水族馆中存在三条鱼——它们的名称、种类和储水箱在这三个元组中注明。

我们的目标是按坦克组织我们的库存——我们想知道每个坦克中存在的鱼的列表。 换句话说,我们想要一个将 "tank-a" 映射到 ["Sammy", "Mary"]"tank-b"["Jamie"] 的字典。

我们可以使用 defaultdict 将鱼按缸分组:

from collections import defaultdict

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]
fish_names_by_tank = defaultdict(list)
for name, species, tank in fish_inventory:
    fish_names_by_tank[tank].append(name)

print(fish_names_by_tank)

运行此代码,我们将看到以下输出:

Outputdefaultdict(<class 'list'>, {'tank-a': ['Sammy', 'Mary'], 'tank-b': ['Jamie']})

fish_names_by_tank 被声明为 defaultdict,默认插入 list() 而不是抛出 KeyError。 由于这保证了 fish_names_by_tank 中的每个键都指向一个 list,我们可以自由调用 .append() 将名称添加到每个坦克的列表中。

defaultdict 在这里可以帮助您,因为它减少了意外 KeyErrors 的机会。 减少意外的 KeyErrors 意味着您的程序可以写得更清晰,行数更少。 更具体地说,defaultdict 成语让您避免手动为每个坦克实例化一个空列表。

如果没有 defaultdictfor 循环体可能看起来更像这样:

没有 defaultdict 的更详细的示例

...

fish_names_by_tank = {}
for name, species, tank in fish_inventory:
    if tank not in fish_names_by_tank:
      fish_names_by_tank[tank] = []
    fish_names_by_tank[tank].append(name)

仅使用常规字典(而不是 defaultdict)意味着 for 循环体始终必须检查 fish_names_by_tank 中给定的 tank 是否存在. 只有在我们确认 tank 已经存在于 fish_names_by_tank 中,或者刚刚用 [] 初始化之后,我们才能附加鱼的名称。

defaultdict 在填充字典时可以帮助减少样板代码,因为它永远不会引发 KeyError

使用双端队列有效地将元素添加到集合的任一侧

Python 列表是可变或可变的有序元素序列。 Python 可以在恒定时间内追加到列表(列表的长度对追加时间没有影响),但是在列表的开头插入可能会更慢——随着列表变大,所需的时间也会增加。

Big O 表示法 而言,追加到列表是一个常数时间 O(1) 操作。 相比之下,在列表的开头插入会因 O(n) 的性能而变慢。

注意: 软件工程师经常使用一种叫做“Big O”的符号来衡量程序的性能。 当输入的大小对执行过程所需的时间没有影响时,就说它以恒定时间或 O(1)(“1 的大 O”)运行。 正如您在上面了解到的,Python 可以附加到具有恒定时间性能的列表,也称为 O(1)

有时,输入的大小直接影响运行过程所需的时间。 例如,在 Python 列表的开头插入,列表中的元素越多,运行速度就越慢。 大 O 表示法使用字母 n 来表示输入的大小。 这意味着将项目添加到 Python 列表的开头以“线性时间”或 O(n)(“n 的大 O”)运行。

通常,O(1) 过程比 O(n) 过程快。


我们可以在 Python 列表的开头插入:

favorite_fish_list = ["Sammy", "Jamie", "Mary"]

# O(n) performance
favorite_fish_list.insert(0, "Alice")

print(favorite_fish_list)

如果我们运行以下命令,我们将看到如下输出:

Output['Alice', 'Sammy', 'Jamie', 'Mary']

列表中的 .insert(index, object) 方法允许我们在 favorite_fish_list 的开头插入 "Alice"。 但值得注意的是,在列表开头插入具有 O(n) 性能。 随着favorite_fish_list长度的增长,在列表开头插入一条鱼的时间会成比例地增长,并且会越来越长。

collections 模块中的 deque(读作“deck”)是一个类似列表的对象,它允许我们在具有恒定时间的序列的开头或结尾插入项目([X171X ]) 表现。

deque 的开头插入一个项目:

from collections import deque

favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])

# O(1) performance
favorite_fish_deque.appendleft("Alice")

print(favorite_fish_deque)

运行此代码,我们将看到以下输出:

Outputdeque(['Alice', 'Sammy', 'Jamie', 'Mary'])

我们可以使用预先存在的元素集合来实例化 deque,在本例中是三个最喜欢的鱼名的列表。 调用 favorite_fish_dequeappendleft 方法允许我们以 O(1) 的性能在集合的开头插入一个项目。 O(1) 性能意味着即使 favorite_fish_deque 有数千或数百万个元素,将项目添加到 favorite_fish_deque 开头所需的时间也不会增长。

注意: 尽管 deque 在序列开头添加条目比列表更有效,但 deque 执行所有操作的效率并不比列表更有效。 例如,访问 deque 中的随机项具有 O(n) 性能,但访问列表中的随机项具有 O(1) 性能。 当从集合的任一侧快速插入或删除元素很重要时,请使用 deque。 Python 的 wiki 上提供了时间性能的完整比较


结论

collections 模块是 Python 标准库的一个强大部分,可让您简洁高效地处理数据。 本教程涵盖了 collections 模块提供的三个类,包括 namedtupledefaultdictdeque

从这里,您可以使用 收集模块的文档 来了解有关其他可用类和实用程序的更多信息。 要全面了解 Python 的更多信息,您可以阅读我们的 如何在 Python 3 中编码教程系列