如何在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
成语让您避免手动为每个坦克实例化一个空列表。
如果没有 defaultdict
,for
循环体可能看起来更像这样:
没有 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_deque
的 appendleft
方法允许我们以 O(1)
的性能在集合的开头插入一个项目。 O(1)
性能意味着即使 favorite_fish_deque
有数千或数百万个元素,将项目添加到 favorite_fish_deque
开头所需的时间也不会增长。
注意: 尽管 deque
在序列开头添加条目比列表更有效,但 deque
执行所有操作的效率并不比列表更有效。 例如,访问 deque
中的随机项具有 O(n)
性能,但访问列表中的随机项具有 O(1)
性能。 当从集合的任一侧快速插入或删除元素很重要时,请使用 deque
。 Python 的 wiki 上提供了时间性能的完整比较 。
结论
collections
模块是 Python 标准库的一个强大部分,可让您简洁高效地处理数据。 本教程涵盖了 collections
模块提供的三个类,包括 namedtuple
、defaultdict
和 deque
。
从这里,您可以使用 收集模块的文档 来了解有关其他可用类和实用程序的更多信息。 要全面了解 Python 的更多信息,您可以阅读我们的 如何在 Python 3 中编码教程系列 。