列表推导是我在Python中最喜欢的功能之一。所以我写了一篇关于它们的文章进行讨论,并在PyCon 2018上进行了3小时的演讲论述。
虽然我很喜欢列表推导,但我发现一旦新的Pythonistas开始真正欣赏、理解之后,他们往往会在很多地方使用它。 列表推导虽然很有用,但很容易被滥用!
本文旨在讨论推导不是工作的最佳工具的情况,至少在可读性方面。 我们将讨论一些更容易理解的替代方案,我们也会看到一些不那么明显的情况,根本不需要推导。
如果你还不是列表推导的“粉丝”那么这篇文章并不是为了吓唬你。 这是为了让那些需要它的我们(包括我自己)适度使用。
注意:注意:在本文中,我将使用术语“推导”来指代所有形式的推导(list,set,dict)以及生成器表达式。 如果你不熟悉推导,我建议你阅读这篇文章或者观看这个演讲(这个演讲更深入地探讨了生成器表达式)。
用很少的间距写推导式
列表推导式的批评声都在说,它们的可读性太差了。他们是正确的,许多推导式的可读性都很差,有时,一个推导式想要变得可读性更好,只需要更好的间距。
例如,先看一下下面的代码:
通过添加一些放置良好的换行符,我们可以使推导式的可读性更好一些。
更少的代码意味着更好的可读性,但也不是一直如此。空格在你写推导式的时候十分重要。
一般来说,我更喜欢使用上面的缩进样式将大多数推导式用多行代码划分出来。
写奇怪的推导式
一些循环在技术上可以被写成推导式,但是他们存在太多逻辑,也许他们并不适合写成推导式。
看一下下面的代码:
上面这个推导式等价于下面这个for循环:
推导式和for循环都使用三个嵌套的内联if语句(Python的三元运算符)。
这里有一个更加可读的方式来写代码,使用if-elif-else结构。
仅仅是有一种方法可以将你的代码写成推导式,这并不意味着你应该将你的代码写成推导式。
在推导式中要小心使用任何数量的复杂逻辑,即使是单个内联if:
如果你真的更喜欢在这样的情况下使用推导式,至少要考虑空格或括号是否可以使代码更具可读性:
并且考虑是否将一些逻辑分解为单独的函数,这样也可以提高可读性
单独的函数是否使事物更具可读性将取决于操作的重要性,操作的规模以及函数名称传达操作的程度。
循环伪装成推导式
有时会遇到使用推导式语法的代码,但是却破坏了推导式的使用精神。
举个例子,下面的代码看起来像推导式
但它并不像是一种推导式。 我们正在将推导用于其不适合的目的。
如果我们在Python shell中执行这个推导式,你会明白我的意思。它除了会打印出1到10的数字之外,还会打印出一堆None。
我们想打印出1到10之间的所有数字,这就是我们所做的。 但是这个推导式也向我们返回了一个无值的列表。
推导式用于构建列表,这是它们的作用。我们从print函数建立了一个返回值列表,print函数返回None。但是我们并不关心我们的推导式所建立的列表,我们只关心它的副作用。
我们可以这样写代码:
列表推导式用于循环遍历可迭代序列并构建新列表,而for循环用于循环遍历可迭代序列以执行您想要的任何操作。
当我在代码中看到列表推导式时,我立即假设我们正在构建一个新列表(因为这就是它们的用途)。 如果您将推导式用于构建新列表之外的目的,则会使阅读代码的其他人感到困惑。所以,如果你不是想构建一个新的列表,不要使用推导式。
在存在更合适的工具时使用推导式
对于许多问题,更特殊的工具比循环的通用目的更有意义。 但是推导式并不总是最适合手头工作的专用工具。
有这样一段代码:
这种理解唯一目的是遍历给定的可迭代文件(csv.reader(csv_file))并从中创建一个列表。但是在python中,我们有更加合适的工具来实现。Python列表构造函数可以为我们完成所有循环和列表创建工作,如下所示(第二行开头应该是lines = list(...)):
推导式是一种特殊用途工具,用于循环遍历迭代以构建新列表,同时修改过程中的每个元素或过滤元素。 列表构造函数是一个专用工具,用于循环遍历迭代序列以构建新列表,而不会更改任何内容。
如果你不需要在列表构建过程中过滤你的元素,或者将它们变成新的元素,那么你不需要列表推导式,而需要列表构造函数。
下面这段代码,这种推导式将我们从zip循环中获得的每个行元组转换为列表:
我们也可以通过列表构造函数来实现:
无论何时你看到推导式是像这样的:
你都可以将其替换成如下代码:
同样适用于字典、集合推导式。
这里有一段之前我写过的代码(倒数第二行是 for abbreviation ....):
在这里,我们循环遍历两项元组的列表并从中创建字典。这个任务正是dict构造函数的用途:
内置列表和字典构造函数不是唯一的推导式替换工具。 标准库和第三方库还包括有时比推导式工具更适合您的循环需求的工具。这是一个生成器表达式,它将一个可迭代序列中的所有元素相加:
以下是使用itertools.chain完成相同的事情:
当你应该使用替代方案并不总是直截了当时,你应该使用推导式。我经常纠结于使用itertools.chain还是推导式。 我通常以两种方式编写代码,然后选择看起来更清晰的代码。可读性在很多编程结构中都是特定于问题的,包括推导式。
不必要的工作
有时您会看到不应被其他构造替换的推导式,而应完全删除,只留下它们循环的迭代序列。
这里我们打开一个单词文件(每行一个单词),将文件存储在内存中,并计算每次出现的次数:
这里我们使用生成器表达式,也可以。
在将它传递给Counter类之前,我们循环遍历列表以将其转换为生成器。 那是不必要的工作! Counter类接受任何可迭代序列它不关心它们是列表,生成器,元组还是其他东西。
这是另一种不必要的推导式:
如果我们要做的就是遍历它一次,没有理由将迭代序列转换为列表。在Python中,我们通常不关心某些东西是否是列表,而是更关心它是否是可迭代的。当你不需要时,小心不要创建新的迭代:如果你只想循环迭代序列一次,只需使用你已经拥有的迭代序列。
我什么时候才能用推导式
简单但不精确的答案是,只要您能够以下面的格式编写代码,并且没有其他工具可以用来缩短代码,您应该考虑使用列表推导。
这个循环可以写成如下推导式:
复杂的答案是,只要推导式有意义,你就应该考虑它们。 这不是一个真正的答案,但是“我应该何时使用推导式”这一问题没有一个答案?例如,这是一个for循环,它看起来并不像是可以使用推导式来重写:
这里也有另外一种方法:使用构造函数:all函数:
再如下面这段代码:
那里没有append,也没有建立新的迭代序列。 但是如果我们创建一个正方形生成器,我们可以将它们传递给内置sum函数以获得相同的结果:
因此,除了“我可以按我的方式从循环复制粘贴到推导式”检查之外,还有另一个更模糊的检查:可以通过生成器表达式与可迭代接受函数或类相结合来增强代码吗?任何接受可迭代序列作为参数的函数或类都可能是与生成器表达式组合的良好候选者。
小心使用列表推导式
列表推导式可以使你的代码更具可读性,但它们肯定会被滥用。
列表推导是用于解决特定问题的专用工具。 list和dict构造函数是用于解决更具体问题的更加特殊用途工具。
当您遇到的问题不符合推导式的适用范围或其他特殊用途的循环工具时,循环是一种更通用的工具。
像any,all和sum这样的函数,以及像Counter和chain这样的类是可接受迭代序列的工具,它们很好地与推导式配对,有时可以完全取代推导式。
请记住,推导式是出于一个目的:从旧的可迭代序列创建一个新的可迭代序列,同时稍微调整值或过滤掉与某个条件不匹配的值。推导式是一个有用的工具,但它们不是你唯一的工具。不要忘记列表和字典构造函数,并在您的推导式不合适时始终考虑循环。
英文原文:https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/
译者:assasi