Python 容器使用的 5 个技巧和 2 个误区

时间:2019-07-30 来源:www.gdlsxny.com

Python中有四种主要类型的内置容器:通过单独或组合使用它们,您可以有效地完成许多事情。

Python语言本身的内部实现细节也与这些容器类型密切相关。例如,Python的类实例属性,全局变量等按字典类型存储。

在本文中,我将从容器类型的定义开始,并尝试总结一些日常编码的最佳实践。然后围绕每种容器类型提供的特殊功能分享一些编程技巧。

我在前面给出了一个简单的“容器”定义:用于容纳其他物体的容器。但是这个定义过于宽泛,无法为我们的日常编程提供任何指导价值。要在Python中真正掌握容器,您需要从两个层面开始:

底层实现:内置容器类型使用什么数据结构?操作如何工作?

高级抽象:什么决定了一个对象是一个容器?什么行为定义了容器?

接下来,让我们站在这两个不同的级别上,重新识别容器。

Python是一种高级编程语言,提供高度封装和抽象的内置容器类型。与名称“链表”,“红黑树”,“哈希表”相比,所有Python内置类型名称仅描述此类型的功能,而其他人无法通过这些名称来理解它们。甚至还有一点内部细节。

这是Python编程语言的优点之一。与更接近底层计算机的编程语言(如C语言)相比,Python重新设计并实现了一种内置容器类型,这种类型对程序员更友好,可以屏蔽内存管理等额外工作。为我们提供更好的开发体验。

但如果这是Python语言的优势,为什么还要理解容器类型的实现细节呢?答案是:注重细节可以帮助我们编写更快的代码。

1.避免频繁扩展列表/创建新列表

所有内置容器类型都不限制容量。如果您愿意,可以继续将增量数字推入空列表中,最终使整个机器的内存爆炸。

在Python语言的实现细节中,列表的内存按需分配[注1]。当列表当前没有足够的内存时,它将触发内存扩展逻辑。分配内存是一项昂贵的操作。虽然在大多数情况下,它不会对程序的性能产生任何严重影响。但是,当您处理的数据量特别大时,由于内存分配,很容易拖动整个程序的性能。

幸运的是,Python早就认识到了这个问题,并提供了一个官方的问题解决指南:“懒惰”。

如何解释“懒惰”?功能的演变是一个很好的例子。

在Python 2中,如果你调用它,你需要等待几秒钟才能得到结果,因为它需要返回一个巨大的列表,并且需要花费大量的时间来分配和计算内存。但是在Python 3中,相同的调用将立即得到结果。因为函数不再返回列表,而是返回类型的惰性对象,所以只有在迭代或切片时它才会返回实际数字。

因此,为了提高性能,内置函数是“懒惰的”。为了避免过于频繁的内存分配,在日常编码中,我们的函数也需要是懒惰的,包括:

更多使用关键字,返回生成器对象

尝试使用生成器表达式而不是列表派生表达式

生成器表达式:

列表派生表达式:

尝试使用模块提供的惰性对象:

使用替代

直接使用可迭代文件对象:而不是

2.将deque模块用于在列表头部操作的场景

该列表基于数组结构(Array)实现。当您在列表的头部插入新成员()时,需要移动其后面的所有其他成员。操作的时间复杂性是。这导致列表头部的成员插入比尾部附加(时间复杂度)要慢得多。

如果您的代码需要多次执行此类操作,请考虑使用collections.deque类型而不是列表。因为deque是基于双端队列实现的,所以无论元素是附加到头部还是尾部,都会增加时间复杂度。

3.使用集合/字典来确定成员是否存在

当您需要确定某个成员是否存在于容器中时,使用集合而不是列表更合适。因为操作的时间复杂度和时间复杂度是。这是因为字典和集合都是基于哈希表数据结构实现的。

提示:强烈建议阅读TimeComplexity - Python Wiki,以了解有关常见容器类型的时间复杂性的更多信息。

如果您对字典的实施细节感兴趣,强烈建议观看Raymond Hettinger的演讲现代词典(YouTube)

Python是一种“鸭型”语言:“当你看到一只鸟像鸭子一样行走,像鸭子一样游泳,像鸭子一样游泳时,这只鸟就可以称为鸭子。”因此,当我们说当一个对象是一个类型时,它基本上意味着该对象满足该类型的特定接口规范,并且可以用作这种类型。所有内置容器类型都是如此。

打开位于集合模块下的abc(“抽象类的缩写”)子模块,以查找所有特定于容器的接口(抽象类)[注释2]定义。我们来看看内置容器类型中内置的接口:

列表(列表):meet,和其他接口

元组:满意,

词典(词典):满意度,[注3]

设置:满足,[注4]

每个内置容器类型实际上是一个满足多个接口定义的复合实体。例如,所有容器类型都满足此接口,这意味着它们都是“可以迭代”的。但相反,并非所有可以迭代的对象都是容器。正如字符串可以迭代一样,我们通常不会将其视为“容器”。

考虑到这一事实,我们将重新审视Python中面向对象编程的一个最重要的原则:接口编程而不是实现。

让我们看一下如何在Python中理解“面向接口的编程”的示例。

有一天,我们收到了一个请求:有一个包含许多用户评论的列表。为了正确显示页面,需要用省略号替换一定长度的所有注释。

这个要求非常好,很快我们编写了代码的第一个版本:

在上面的代码中,函数将列表作为参数,然后迭代它,替换需要修改的成员。所有这一切似乎都是合理的,因为我们收到的最原始的要求是:“里面有一个清单.”。但是如果有一天,我们得到的评论不再被列入清单,而是放在不可变的元组中?

在这种情况下,现有的功能设计将迫使我们编写如此缓慢而丑陋的代码。

容器接口编程

我们需要改进功能以避免这个问题。因为函数严重依赖于列表类型,所以当参数类型变为元组时,当前函数不再适用(原因:为赋值抛出异常)。如何改进这部分的设计?秘诀是让函数依赖于“可迭代对象”的抽象概念而不是实体列表类型。

使用generator属性,该函数可以更改为:

在新函数中,我们将列表中依赖的参数类型更改为可迭代的抽象类。这样做有很多好处。其中一个最明显的是,无论注释来自列表,元组还是文件,都可以轻松满足新功能:

在将依赖关系从特定容器类型更改为抽象接口之后,该函数的适用方面变得更宽。此外,新功能在执行效率方面也更有利。现在让我们回到上一个问题。从高层次的角度来看,什么定义了容器?

答案是每个容器类型实现的接口协议定义容器。不同的容器类型在我们眼中,应该是各种特征的组合。在编写相关代码时,我们需要更加关注容器的抽象属性,而不是容器类型本身,这可以帮助我们编写更优雅,更具伸缩性的代码。

提示:您可以在itertools内置模块中找到有关处理可迭代对象的更多宝藏。

有时,我们的代码中有三个以上的分支。就像这样:

以上功能没有发现太多问题,很多人会写类似的代码。但是,如果仔细观察它,可以在分支代码部分找到一些明显的“边界”。例如,当函数确定是否应该以“秒”显示特定时间时,使用它。当判断是否应该在几分钟内使用时,使用它。

从边界完善法律是优化此代码的关键。如果我们将所有这些边界放在一个有序元组中,那么匹配二进制搜索模块bisect。整个功能的控制流程可以大大简化:

除了使用元组来优化太多分支之外,在某些情况下,字典可用于执行相同的操作。关键是要从现有代码中找到重复的逻辑和规则,并尝试更多。

动态解包操作是指使用OR运算符来“解包”可迭代对象。在Python 2时代,这个操作只能在函数参数部分使用,对订单和数量有严格的要求。使用场景非常简单。

但是,在Python 3之后,特别是在3.5版之后,大大扩展了使用场景。例如,在Python 2中,如果我们需要合并两个字典,我们需要这样做:

但是,在Python 3.5及更高版本中,您可以使用运算符快速完成字典合并操作:

此外,您可以在正常赋值语句中使用运算符来动态解包可迭代对象。如果您想了解更多信息,可以阅读下面推荐的PEP。

提示:两个推动动态解包场景扩展的PEP:

PEP 3132 - 扩展的可迭代解包| Python.org

PEP 448 - 附加拆包概括| Python.org

这个副标题可能有点尴尬,让我简单解释一下:“获得许可”和“请求宽恕”是两种不同的编程风格。如果您使用经典要求:“计算列表中单个元素的出现次数”作为示例,两种不同样式的代码将如下所示:

整个Python社区都明确偏爱第一种Ask for Forgiveness的异常捕获编程风格。这件事情是由很多原因导致的。首先,在Python中抛出异常是一个非常轻量级的操作。其次,第一种方法在性能方面优于第二种方法,因为每次循环时都不需要进行额外的成员检查。

但是,示例中的两段代码在现实世界中非常罕见。为什么?因为如果要计算统计数量,可以直接使用它:

此类代码不需要“获得许可”或“请求原谅”。整个代码的控制流程变得更加清晰和自然。因此,如果可能的话,尝试找出省略那些非核心异常捕获逻辑的方法。一些提示:

使用字典成员时:使用类型

或使用内置函数

如果删除字典成员,请不要关心它是否存在:

调用pop函数时设置默认值,例如

当字典获得成员时指定默认值:

列表中不存在的切片访问不会引发异常:

诸如“成员”之类的要求。

字典和集合的结构特征确保其成员不会重复,因此它们通常用于重复删除。但是,使用它们丢失原始列表的结果将丢失。这由底层数据结构“哈希表”的特征决定。

如果你需要沉重并且必须保留订单怎么办?我们可以使用模块:

提示:在Python 3.6中,默认字典类型修改了实现并且已经变为有序。在Python 3.7中,此功能已从语言的实现细节更改为可依赖的正式语言功能。

但我认为整个Python社区需要一些时间才能适应这一点。毕竟,“字典是无序的”或者它是印在无数的Python书籍上的。因此,我仍然建议在有序字典的地方使用OrderedDict。

在本文前面,我们提到了使用“懒惰”生成器的好处。但是,一切都有其两面性。发电机的最大缺点之一是它会干涸。当你完全遍历它们时,在重复遍历之后你将不会获得任何新内容。

不仅生成器表达式,而且Python 3中的map和filter内置函数具有相同的特性。忽略此功能很容易导致代码中出现一些无法检测到的错误。

Instagram在将项目从Python 2迁移到Python 3期间遇到了这个问题。他们在PyCon 2017上分享了一个故事来处理这个问题。访问PyCon 2017的演示摘要中的文章Instagram,搜索“iterators”以查看详细信息。

这是许多Python初学者将犯的错误。例如,我们需要一个函数来删除列表中的所有偶数:

您是否注意到结果中的额外“8”?在遍历列表时修改它时会发生这种情况。因为在循环期间修改了被迭代的对象。遍历下标正在增长,列表本身的长度同时缩小。这将导致列表中的某些成员根本不被遍历。

因此,对于此类操作,请使用新的空列表来保存结果,或使用生成器返回。而不是修改迭代列表或字典对象本身。

在本文中,我们从“容器类型”的定义开始,并在底层和顶层探索容器类型。接下来是一系列文章传统,它提供了一些编写容器相关代码的技巧。

让我们总结一下:

理解容器类型的底层实现可以帮助您编写性能更好的代码

在需求中细化抽象概念,面向接口而不是编程

更多地使用“懒惰”对象并生成较少的“紧急”列表

使用元组和字典简化分支代码结构

使用迭代器函数可以有效地完成很多事情,但是你还需要注意“耗尽”问题

集合和itertools模块中有许多有用的工具。来吧看看吧!

每个人在学习python时都会遇到很多问题,以及对新技术的追求,这里我们推荐我们的Python学习演绎qun:784758214,这里是python学习者的聚集地!同时,我是一名高级Python开发工程师,从基本的python脚本到Web开发,爬行,django,数据挖掘等,从零基础到面向项目的数据。给每个python合作伙伴!分享一些每天需要关注的学习方法和小细节

点击:python技术共享交换