想理解Python的列表解析吗?Think in Excel or SQL.

657 查看

推导式Python 中最有用的设计之一。它融合了老的、可靠的“map”和“filter” 函数到一段紧凑的代码,它语法优雅,允许我们在小段代码中表达复杂的想法。推导式是 Python 高手工具箱里面一个最重要的工具。

然而,我发现很多 Python 程序员,包括一些有经验的开发者,无法完全适应推导式。这有两个原因:第一,对于什么时候使用推导式和它们解决哪种问题并不明显。另外一个同样重要的原因是,它的语法很难被人记住和理解。

我已经开始在我的 Python 课程 中使用关于推导式的新的解释和介绍,并且发现这有助于使低年级学生的学习曲线变得不那么陡峭。在这篇文章中,我将公开我的讲解内容,希望有助于 Python 开发者理解什么时候、在哪里和如何使用推导式。

举个简单的例子:我想输入一个含有 5 个整数的列表,并且得到含有这 5 个数的平方的列表。如果将这个问题给一个初级(甚至是中级)Python 程序员,答案可能会类似这样:

现在的问题是,这种方法的确奏效。(在我的课堂中,我经常会使用这个短语:“很不幸,这种方法奏效。”)通常,当我讨论推导式时,我会讨论函数编程,不可变数据结构的理念,以及我们不愿意改变数据的这一理念。还有在 mapreduce 方面思考的好处。

让我们忘记上面的东西,并且问你一个更加简单的问题:如果你将这个问题给你的会计师,他们会如何解决该问题呢?

几乎可以肯定,一个会计师会打开 Excel,并且将数字放到同一列:

假设上面的数字位于电子表格的 A 列。Excel 使用者会这样做,即告诉 Excel B 列应该是 A*A 的计算结果。问题就得到解决了:

你可以说在这里,不同点是 Excel 有 GUI,而 Python 没有。但是这不是关键点。真正的差异是我们的会计师告诉 Excel 如何将第一列转化成第二列,Python 开发者则编写程序来描述如何执行这个转化。

我们也可以用不同的方式思考这个问题:会计师使用一种并行的方式,将一条表达式应用于一个大型数据集上,而不是串行地解决这个问题,如上面的 for 循环。Excel 使用者不关心,甚至不知道,传递给表达式的数字的顺序。重要的是对于每个数字,只会使用表达式一次,和最后的结果以正确的顺序呈现。

我们可能会取笑 Excel,并且视它的使用者为技术新手。当然,很多 Excel 使用者会否认他们拥有高级的编程能力。但是这种思维,对于 Excel 使用者是如此的基础和自然,而对于程序员是如此陌生。这很可惜,因为这种思维让我们用一种简单的方法表达大量的想法。

总结一下这种方法:

  • 把你的输入当作可迭代的数据源
  • 想一下对于数据源的每个元素,你要使用什么操作
  • 输出一个新的序列

这是传统的 “map” 函数做的事情。Python 的确有一个 “map” 函数,但是今天,我们有代表性地使用列表推导式。

更具体一点,使用我上面使用过的例子:假设我们有一个包含 5 个数字的列表,并且我们想要把那个列表变成它的平方的一个列表。那么列表推导式的语法看起来会像下面一样:

呀。难怪大家会被这个语法吓跑。下面我们把上述语法拆分一下:

  • 首先,这样将返回一个列表。(这就是为什么它被叫做“列表推导式”。)那是因为方括号具有强制性,并且会告诉 Python 创建哪种对象。
  • 数据源是 “range(5)”,它会返回一个列表。
  • 数据源中的每个元素会依次赋值到可迭代变量 “number” 中。
  • 我们会对数据源的每个元素都调用 “number * number” 运算。

换句话说,我们正创建一个新的列表,该列表的元素是让数据源的每个元素都应用了表达式的结果。这听起来像前面我们的会计师所做的一样,使用 Excel:我们告诉 Python 我们想要什么,和如何将源转化成结果。至于内部是如何工作?如何创建列表?我们不知道也不关心。

列表推导式会让人感到畏惧而不容易被理解,部分原因是运算的顺序似乎不常见。我发现用下面的方法来重写列表推导式会对理解有所帮助:

没错——现在我将列表推导式展开成两行;第一行描述了我想调用的运算,第二行描述了数据源。如果这似乎还不熟悉,那么尝试一下把它放到你可能有经验的一些场景中:

虽然他们不是直接等效,但是在一个 SQL 的 SELECT 查询(SELECT 表达式和 FROM 子句的位置)和我们的列表推导式之间有相当多的相似点。一个 SQL 查询的 FROM 子句描述数据源,通常会是一个表格,但是也可以是一个视图或者一个函数调用的结果。SELECT 的初始部分通常是列的名字,但是可以包括函数调用和运算符。

一方面,SELECT-FROM 组合似乎简单到不值得一提。因为你仅需从数据源中获取一组选择好的值即可。另一方面,这样的查询建立起数据库的主干。同样,这样的功能建立起很多 Python 程序的主干,遍历一个数据结构,取出数据结构的一部分,变换那部分,然后返回一个新的列表。

一个我喜爱的例子(也是我的电子书《Practice Makes Python》中的一个练习)是获取 Unix 中使用的 /etc/passwd 文件,然后获取包含在该文件中的用户名。/etc/passwd 文件每行含有一个记录,字段用冒号隔开。这是我电脑里 /etc/passwd 文件的几行:

我们通常会将一个文件看做字节的集合,当我们阅读它的时候,我们赋予它语义意义。但是在 Python 里,我们鼓励将一个文件看成一个有序的、可迭代的文本行的集合。没错,我可以根据字节阅读一个文件,但是想要阅读文件的行是那么的平常,Python 也提供了一些方法来读取文本行。

我们知道我们可以遍历一个文件的行:

这表明了一个文件时可迭代的,即意味着它可以充当一个列表推导式的数据源。这意味着上面的代码可以重写成:

再次,我们的列表推导式的第一行表示我们想要应用到数据源中每个元素的表达式。在这里,表达式就是 line。如果我们想要从这些行获取每一行中的用户名,我们只需使用 string 的 “split” 方法,返回一个列表——然后从结果列表中获取索引为 0 的值。例如:

再次,我们可以从一个 SQL 查询的角度来思考它:

当然,上面的“username”是一个列名。对于我的列表推导式,一个更加等效的查询是带有“info”列的“Users”表,如下:

注意到在这个例子中,我使用了内置 PostgreSQLsplit_part” 运算符来执行等效于 Python 中 str.split 方法的操作。

记住在我的 SQL 查询例子中,一个查询的结果总是看起来和表现得像一个表。返回的列的数量和类型依赖于在 SELECT 语句中表达式的数量和类型。但是结果的集合会有一列或多列,零行或者多行。

同样,一个列表推导式的结果总是一个列表。在列表推导式里,你可以拥有任何你想要的表达式;表达式代表列表中的一项,不是列表本身。

例如,假设我想把 /etc/passwd 里的用户名变成一个字典列表。这里不需要一个创建单个字典的字典推导式。而是需要一个列表推导式,它的表达式创建一个字典。下面是符合上述内容的一个愚蠢的列表推导式:

上述的代码是奏效的,它创建了一个字典列表。每个字典有一对键-值对。但是上面的做法似乎有点愚蠢,而我很可能想得到一个包含用户名和数字用户 ID 的字典,该 ID 处于索引 2 的位置。那么,我就可以这样写:

再次,我们可以从 Excel 的角度去思考,或者是 SQL 的角度:现在,我的查询产生了一列结果,但是每列包含一个文本字符串。我们甚至可以说查询产生两列结果,这在 SQL 的世界里是非常正常的。

请忽略在一个推导式中调用两次 str.split 的效率(或没有):当我在 Mac 中运行这段代码时,它产生一个异常,抱怨一个索引超出了范围。

原因很简单:我根据 : 分离每一行,并将分离后的结果放到一个列表中。但是如果有一行没有包含任何 : 字符,那么将返回一个单元素列表。因此我需要除去那些不符合的行。特别地,至少在我的 Mac 中,我需要删除 /etc/passwd 里面注释的行,即以‘#’字符开头的行。

在列表推导式的世界里,我会像下面这样写:

与先前的 SQL 做进一步类比,在 Python 代码中添加等效的 SQL 语句注释: