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

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程序员媒体

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java面试通关手册

最最最常见的Java面试题总结——第二周

String类中使用字符数组:private final char value[]保存字符串,所以String对象是不可变的。StringBuilder与Str...

1152
来自专栏编程

Python函数之匿名函数

各位小伙伴,周五快乐! 今天是2017年最后一个工作日,大家都在忙碌些什么呢? 今天我们要讲的是Python函数中的匿名函数 好像函数中的分类及说法很多,但是大...

2086
来自专栏owent

再议 C++ 11 Lambda表达式

C++ 11 标准发布,各大编译器都开始支持里面的各种新特性,其中一项比较有意思的就是lambda表达式。

1062
来自专栏coding for love

JS入门难点解析3-作用域

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

872
来自专栏程序员互动联盟

【专业技术】C++里面重要的几个关键字的用法

编者按: 这几个关键字非常重要,程序中经常见到他们的身影,但是确切意思有时候还需要多搜索下才能知道。笔者这里把它搬出来,也是希望大家引起重视,努力掌握它。 C+...

3617
来自专栏java一日一条

Java集合框架综述

近被陆陆续续问了几遍HashMap的实现,回答的不好,打算复习复习JDK中的集合框架,并尝试分析其源码,这么做一方面是这些类非常实用,掌握其实现能更好的优化我们...

831
来自专栏吴裕超

es6 Object的几个新方法

ES5 的 Object.preventExtensions 则可以阻止给对象添加新属性

1023
来自专栏CSDN技术头条

Python编程中的反模式

这篇文章收集了我在Python新手开发者写的代码中所见到的不规范但偶尔又很微妙的问题。本文的目的是为了帮助那些新手开发者渡过写出丑陋的Python代码的阶段。为...

2076
来自专栏小俊博客

Nginx的location规则迷之匹配

Nginx,一个改变世界的软件,其作者是一个俄罗斯人,俗称毛子,在国人的印象中,是一群晚饭后牵着大灰熊在小区楼下散步的彪汉。能写出这般顺滑的软件,可谓是心有猛虎...

6112
来自专栏web前端教室

浅谈数据结构 - 字典

浅淡,真的是很浅。Orz.. 先摆出定义,这里的字典是啥样的? 是以键-值对形式保存数据的一种结构。 现实中比较典型的例子,就是以前的电话本。你想找一个单位的电...

19510

扫码关注云+社区

领取腾讯云代金券