Python的集合(collections)模块,为很多用其他方法很难实现的场景提供了解决方案。
本文我们将会学习该模块的抽象概念是如何产生的,日后处理不同问题的过程中迟早会用得到这些知识。
扩展内置类型
有时,我们需要使一个对象具备Python内置类型的功能,在此基础上还需要增加一些功能。为了达到这个目的,最通用的方法是直接子类化该类。
例如,设想一个将事件建模为字典的事件系统,对此我们需要另外构建事件的元数据。类似下列代码可能是我们的首选方法:
试着运行以上代码,将会发现已经可以实现一些能够想到的基本功能:
然而,仅仅这样是不够的,我们想让它和字典完全兼容,换句话说就是让它真正成为字典类型。接下来就出会先一堆问题,当然也有对应的解决方案!首先迭代一下该对象的键和值来看一下:
我们期望的返回值为定义过的转换(包含每个事件类的前缀),但很遗憾,我们只得到字典的基本值,忽略了我们自定义的__getitem__() 实现。结果如下所示[3]:
导致结果和预期有差异的原因是items() 方法没有调用我们自己实现的__getitem__() 方法。相反,使用底层的C实现将不会搜索同一对象中定义的其他方法。
另一个例子: 假如现在我们想要以常规格式记录事件,为了实现这个目的我们编写了如下的函数,需要从字典中获取参数。
再次提醒,我们想让自定义的对象成为字典,因此使用 ** 将会正常运行,但这次还是没有调用我们的方法。
如果我们继承collections.UserDict,所有上面的问题都将迎刃而解。如果想在代码中创造类似字典的对象,它是最合适的选择。
只需要这个定义,前面的所有例子都可以顺利运行(items 方法,关键字参数等等)。它实现的细节大家没有必要搞懂,只需要了解该对象是对字典的封装(称作data),当其方法被重写时,也将应用于封装起来的data。不需要访问data属性,对象自己就会表现的像字典一样。
不止字典,该准则同样适用于列表、字符串等。对每种情况,当需要创建子类时,分别使用collections.UserDict,collections.UserList,collections.UserString。
一个函数返回多个值
函数总会返回一些东西。当函数返回不止一个值时,实际上是创建了一个元组并将其返回(重申一下,还是一个值)。
当返回值的数量越来越多,代码的可读性将会越来越差。按照经验来说,当返回值超过3个时,最好使用命名元组(namedtuples)来取代普通的写法。
对比下这两种方法在可读性方面的差别:
或者:
第一个例子中,当其他人调用函数来获取数据时,需要猜或者提前被告知返回值的参数以及顺序[1]。只有了解了以上这些内容,才能在调用函数时对返回值进行解包(由于必须知道username == row[0],获取元组将变得更糟糕)。
使用命名元组的例子中,首先很明显就能知道返回值是一个特殊类型的对象,通过查看对象的定义就可以了解其包含的数据及数据的访问方法。
更高效、紧凑得计算
工作中,经常遇到的需求之一是计算数据源中元素出现的次数。这种类型的映射关系首选的数据结果自然是字典。用伪代码标识大概是这样的:
然而,这看起来并不符合Python风格。更具有Python风格的实现应该充分利用标准库:
短短一条语句,提供了一个满足我们要求的类字典对象。
该命令的参数可以是任何可迭代对象,它将遍历该对象,将其中元素的唯一值和其出现的次数一一对应。
例如,为了计算每个类型的事件,我们可以传入一个内联生成器,如下:
上例中第一个比较简单粗暴的实现使用了字典的默认值。类似于occurrences.setdefault(element, 0)这样的功能,有一种效果更好的实现方式:使用collections.defaultdict。
创建字典的同时创建一个可调用对象,当键不存在时则调用该对象。这比每次都设置字典的值更简洁、高效。
本例中,如果我们想以时间类型分组,可以创建以下代码中的映射:
栈是另外一个在解决多种问题上迟早会用到的数据结构。Python中的列表可以用在很多地方。但如果像栈或者队列一样使用列表,在Python中就是反面教材了。尽量避免使用以下代码:
列表的这些操作的复杂度都是O(n),用其他方法能获得巨大改善。
collections.deque正是为这类操作而生的,使用类似.append(element)或者.popleft() 这样的操作符,其复杂度都只是O(1)。这样写不仅仅只是更符合语言习惯,更重要的是效率的提升[2]。
其他有用的类
最后两个类颇富争议,不少人都觉得已经不那么重要,但它们仍然值得探索一下。
第一个是映射链(ChainMap)类。它接收参数传递的多个映射对象,并生成一个新的映射对象。当原始映射的值发生变化,映射链的值也随之变化。有人可能认为,既然从Python 3.5开始,已经可以通过使用 new_dict = {**d1,**d2} 这样的语法来拼接出新字典,ChainMap 也就没什么用了。不过,他们之间还是有些许区别。
Python 3.5及以上版本引入的新语法仅仅解决了从其他字典创建新字典的问题,但映射链还有其他功能:
如代码所示,我们通过event 和 context的键创建了enriched_event。这个操作按顺序遍历了所有字典,通过键取得对应的值并放入新的字典中。如果对源字典进行修改,这些修改并不会体现在enriched_event中(它已经被创建,完全是一个新对象了)。然而,使用映射链的话,源字典的修改会引起映射链对象的变化,反之亦然。
可以把它看做是其他多个映射对象的一个视图。
第二个是有序字典(collections.OrderedDict)类,通常被用来保存字典的键的顺序。最开始的几个版本,需要使用类似[(key, value),...]的语句,传入包含两个元素的元组组成的列表来创建。从Python 3.6之后,关键字参数的顺序可以指定了,只需要像普通字典一样创建,生成的字典也会按照顺序排列。这也正是很多人认为有序字典类已经有些过时的原因:而事实并非如此,关键字参数保存的顺序正是Python字典的顺序。也就是说,Python 3.6 及之后版本使用的字典类(dicts) 某种意义上来说其实就是现在所讲的有序字典(OrderedDict)类。
结论
文本总共探讨了集合的以下几点:
1、不要直接子类化内置类型:需要的时候优先使用UserDict 、UserList或者UserString。直接对内置类型进行子类化将会产生一些很难第一眼定位、调试的未知错误。
2、当需要给多个值进行分类,或者函数需要返回多个参数时,使用 命名元组(namedtuple)。
3、充分利用Counter 和 defaultdict 的特性来解决通用问题。
4、切忌过度使用列表, 别拿列表做其他尝试。对于栈或者队列操作,使用collections.deque。
5、有序字典(OrderedDicts)已经是过去式[4]。使用映射链(ChainMaps)吧!
通过抽象基类(abstract base clases),集合类(collections)包含了处理类型的模块。和第一部分提到的比较周全的应用类似:在检查类型时更倾向于使用该界面。例如,使用isinstance(my_dict, collections.abc.MutableMapping)代替isinstance(my_dict, dict)。原因如下:如果my_dict 不是字典而仅仅是类字典的对象(例如,可能是collections.UserDict 或者ChainMap生成的实例等),前一种方法将获得正确的结果,而后一种就会出问题。
总而言之,collections 模块是提升效率的重要来源,能帮助我们写出更符合Python习惯、更高效的代码。
引用和注记
引用
本文的引用如下:
集合类(Colletions) 文档:https://docs.python.org/3/library/collections.html
Pypy上关于CPython中内置类型子类化的区别的文 http://doc.pypy.org/en/latest/cpython_differences.html#subclasses-of-built-in-types
Python中数据结构的复杂性:https://wiki.python.org/moin/TimeComplexity
“Fluent Python” — Luciano Ramahlo
Python 3.6 中更简洁的字典实现 https://bugs.python.org/issue27350
注记:
[2]. https://wiki.python.org/moin/TimeComplexity
英文原文:https://ogmcsrgk5.qnssl.com/vcdn/1/%E4%BC%98%E8%B4%A8%E6%96%87%E7%AB%A0%E9%95%BF%E5%9B%BE/using-collections-in-python-36129737b5a1.png
译者:woody
领取专属 10元无门槛券
私享最新 技术干货