前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >让你比95%的人更懂Pythonic的内置模块:collections

让你比95%的人更懂Pythonic的内置模块:collections

作者头像
企鹅号小编
发布2018-02-11 14:48:44
7470
发布2018-02-11 14:48:44
举报
文章被收录于专栏:编程编程

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

本文来自企鹅号 - Python程序员媒体

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文来自企鹅号 - Python程序员媒体

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档