1.3.1 集合

当你选择一个集合(collection)时,你就是在用这个集合传达特定的信息,你必须为手头的任务选择正确的集合。否则,维护人员将从你的代码中推断出错误的意图。

思考下面这段代码,它获取一个烹饪书列表,并提供作者与已写书籍数量之间的映射:

我对集合的使用说明了什么?为什么我不传递字典或集(set)?为什么我不返回列表?基于我目前使用的集合,你可以假设:

•我传进来一份烹饪书列表。在这个列表中可能会有重复的烹饪书。

•我返回字典类型。用户可以查找特定的作者,或者遍历整个字典。我不必担心返回的集合中有重复的作者。

如果我想要传达不应该将重复信息传递到这个函数中,该怎么办?列表传达了错误的意图。相反,我应该选择一个集来表示此代码绝对不会处理重复信息。

选择集合可以告诉读者你的具体意图。以下是一些常见的集合类型,以及它们传达的意图:

列表

这是一个用来迭代的集合。它是可变的,可以在任何时候更改。很少会期望从列表的中间检索特定的元素(使用静态列表索引),里面可能有重复的元素。书架上的烹饪书可以用列表的形式存储。

字符串

不可变的字符集合。烹饪书的名字可以是一个字符串。

生成器

一个用来迭代的集合,而不是被索引的集合。每个元素访问都是被惰性执行的,所以每个循环迭代都可能会花费时间和资源。对于计算昂贵或无限的集合来说,它们非常有用。一个菜谱的在线数据库可能会作为生成器返回,当用户只查看搜索的前10个结果时,你不需要获取世界上所有的菜谱。

元组

一个不可变的集合。元组不会发生变化,因此更有可能从中提取特定的元素(通过索引或解包)。它很少被迭代。关于特定烹饪书的信息可以表示为一个元组,例如(cookbook_name,author,pagecount)。

不包含重复项的可迭代集合,但是不能依赖于元素的顺序。烹饪书中的配料可以作为一个集存储。

字典

从键到值的映射。键在整个字典中是唯一的。字典通常能迭代访问,或使用动态键建立索引。一个烹饪书的索引是键到值映射(从主题到页码)的一个很好的例子。

不要使用与你的意图不符的集合。我经常遇到不应该有重复项的列表,或者没有实际用于将键映射到值的字典。每当你的意图与代码中的内容脱节时,就会产生维护负担。维护人员必须暂停一下,找出你真正想表达的意思,然后绕过错误的假设。

动态索引与静态索引

根据你所使用的集合类型,你可能希望或不希望使用静态索引。静态索引是使用一个常数来索引到集合中,例如my_list[4]或my_dict["Python"]。一般来说,列表和字典通常不需要这样的用例。由于它们的动态特性,你不能保证这个集合在那个索引上有你要找的元素。如果你正在这些类型的集合中寻找特定的字段,这是一个好迹象,说明你需要一个用户定义的类型(在第8~10章中讨论)。对元组设置静态索引是安全的,因为它们是固定的大小。而集和生成器不会设置索引。

例外情况包括:

•获取序列的第一个或最后一个元素(my_list[0]或my list[-1])。

•使用字典作为中间数据类型,例如读取JSON或YAML时。

•处理特定固定块的序列操作(例如,总是在第三个元素后分割或检查固定格式字符串中的特定字符)。

•特定集合类型的性能原因。

相反,动态索引用一个变量(这个变量只有运行的时候才知道)索引到一个集合。对于列表和字典来说,这是最合适的选择。在遍历集合或使用index()函数搜索特定元素时,你会发现这一点。

这些都是基本的集合,但有更多的方式来表达意图。以下是一些特殊的集合类型,它们在与未来的沟通中更具表现力:

frozenset

不可变的集合。

orderedDict

基于插入时间保持元素顺序的字典。从CPython 3.6和Python 3.7开始,内置字典也将根据插入时间保留元素的顺序。

defaultdict

在键缺失时提供默认值的一个字典。例如,可以重写之前的例子如下:

这为最终用户引入了一种新的行为——如果他们在字典中查询一个不存在的值,将返回0。在某些情况下,这可能是有益的,但如果不是,你可以直接返回dict(counter)。

Counter

一种特殊类型的字典,用于计算一个元素出现的次数。这可以大大简化前面的代码,如下所示:

花一分钟想想最后一个例子,看看使用Counter如何可以在不牺牲可读性的情况下使代码更加简洁。如果你的读者熟悉Counter,那么这个函数的含义(以及实现方式)是显而易见的。这是通过更好地选择集合类型来向未来传达意图的一个很好的例子。我将在第5章进一步探讨集合。

还有许多其他类型需要研究,包括array、bytes和range。无论何时遇到一个新的集合类型(无论是内置的还是其他类型),问问自己它与其他集合有什么不同,它向未来的读者传达了什么信息。