·发表于 Towards Data Science ·阅读时长 9 分钟·2023 年 3 月 21 日
--
作者提供的图片。
你是否曾在雨天通过 Netflix 滚动,感到被无尽的电影和节目选择所压倒?
在编程中,选择的悖论可能同样令人不知所措。面对如此众多的库和框架,提供了无数种实现相同目标的方法,容易在选择的海洋中迷失方向。
在 Python 中,这种情况通常出现在程序员需要在函数式编程方法(如内置函数 map()
、filter()
和 reduce()
)与更具 Python 风格的列表推导式之间做选择时。
在这篇文章中,我们将通过语法、可读性和性能的视角探讨这两种不同方法的优缺点。
在 Python 中,列表推导式是一种简洁的方法,它基于已存在的列表生成一个新列表。简单来说,它本质上是一个for 循环的一行代码,并可以在末尾包含一个if 条件。语法可以分解为如下:
作者提供的图片。
假设我们有一个名为 numbers
的数字列表,我们希望从中选取偶数并对其平方。现在,老旧的方式是这样的:
squared_numbers = []
for number in numbers:
if number % 2 == 0:
squared = number ** 2
squared_numbers.append(squared)
然而,使用列表推导式,我们可以在一行代码中完成这一操作:
squared_numbers = [i**2 for i in numbers if i % 2 == 0]
无论哪种方式都能得到相同的结果,但列表推导式提供了一个更清晰、更可读的解决方案,因为其语法字面上是:*“*做这个 对于 每个值 在 这个列表 如果 这个条件 满足”。
一般来说,列表推导式通常比常规的 for
循环更快,因为它们不需要在每次迭代时查找列表并调用其 append
方法。
现在我们对列表推导式有了比较好的理解,接下来我们来看看它们与一些常用的内置函数(如 map()
、filter()
和 reduce()
)相比如何。这就是我之前提到的选择悖论。程序员往往知道这些方法的存在,但该选择哪一个呢?
让我们逐一了解每个内置函数,并将它们与 Pythonic 对应的列表推导式进行比较。
如果你的目标是对可迭代对象(如列表)中的每一项应用转换函数,那么 map()
函数是一个很好的起点。其语法相当简单,只需要两个输入参数:(1) 一个转换函数,以及 (2) 一个可迭代对象(即你的输入列表)。
假设我们有一个与欧元对应的数字列表,我们希望将它们转换为美元。这可以通过以下方式完成:
>>> eur = [1, 2, 3, 4, 5]
>>> usd = list(map(lambda x: x / 0.939276, eur))
>>> usd
[1.0646497940967299,
2.1292995881934598,
3.1939493822901897,
4.2585991763869195,
5.323248970483649]
注意,我们必须在这里明确指定 list()
函数,因为 map()
本地返回的是一个迭代器——一个 map 对象。
还要注意,map()
允许你使用这些匿名的、即兴的 lambda 函数,这些函数允许你即时定义一个函数。如果你想了解更多关于 lambda 函数、它们的语法以及如何使用它们的内容,可以查看以下文章:
## 如何在数据科学中有效使用 Python 的 Lambda 函数
towardsdatascience.com
你可能已经注意到,相同的任务也可以通过列表推导式来完成。那么让我们看看它们在可读性和性能方面的比较。
具体来说,我们将讨论三种场景:(1) 列表推导式,(2) 使用预定义输入函数的 map()
,以及 (3) 使用即兴的 lambda 函数的 map()
。
# predefined conversion function
def eur_to_usd(x):
return x / 0.939276
>>> lst = list(range(1000000))
# list comprehension
>>> %timeit -r 10 -n 10 [i / 0.939276 for i in lst]
163 ms ± 4.96 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)
# map with predefined input function
>>> %timeit -r 10 -n 10 list(map(eur_to_usd, lst))
197 ms ± 4.33 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)
# map with lambda function
>>> %timeit -r 10 -n 10 list(map(lambda x: x / 0.939276, lst))
204 ms ± 4.28 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)
就简洁性和可读性而言,列表推导式在这里似乎赢得了比赛。程序员的意图立即显现出来,不需要额外的关键字或定义额外的函数。然而,值得注意的是,对于更复杂的操作,可能需要定义单独的转换函数,这将削弱列表推导式通常因其可读性而获得的一些优势。
就性能而言,上述示例清楚地表明,列表推导式是最快的,其次是使用预定义输入函数的map()
,最后是使用 lambda 函数的map()
。
关于使用临时 lambda 函数的问题是:它会为输入列表中的每个项目调用,导致计算开销,因为 lambda 函数对象的创建和销毁,最终导致性能下降。相比之下,预定义函数经过优化并存储在内存中,这使得执行更为高效。
在性能方面,列表推导式明显优于map()
。此外,它们的语法易于阅读,通常被认为更直观,并且被认为比源自函数式编程的map()
更具 Python 风格。
filter()
函数允许你根据给定条件选择可迭代对象的一个子集。与map()
类似,它需要两个输入参数:(1)过滤函数,通常是lambda 函数,以及(2)一个可迭代对象。
以下是一个示例,我们过滤掉所有奇数,只保留偶数:
>>> numbers = [1, 2, 3, 4, 5]
>>> filtered = list(filter(lambda x: x % 2 == 0, numbers))
>>> filtered
[2, 4]
类似于map()
,我们必须明确声明我们希望返回一个列表,因为filter()
原生返回一个迭代器对象。
让我们看看内置filter()
函数的性能差异,再次使用预定义输入函数和 lambda 函数,并与列表推导式进行比较。
# predefined filter function
def fil(x):
if x % 2 == 0:
return True
else:
return False
>>> lst = list(range(1000000))
# list comprehension
>>> %timeit -r 10 -n 10 [i for i in lst if i % 2 == 0]
84.6 ms ± 2.24 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)
# filter with predefined filter function
>>> %timeit -r 10 -n 10 list(filter(fil, lst))
134 ms ± 6.39 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)
# filter with lambda function
>>> %timeit -r 10 -n 10 list(filter(lambda x: x % 2 == 0, lst))
159 ms ± 6.67 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)
就可读性而言,对于map()
的说法同样适用于filter()
:列表推导式相当易于阅读,不需要任何预定义或临时函数或额外的关键字。然而,有人认为使用filter()
函数会立即展示程序员的意图,即过滤某物,可能比列表推导式更直接。当然,这是一项高度主观的事项,取决于个人的偏好和品味。
就性能而言,我们看到的结果与map()
获得的类似。列表推导式是最快的,其次是使用预定义过滤函数的filter()
,最后是使用临时 lambda 函数的filter()
。这再次是由于 lambda 函数需要在运行时创建新函数对象所带来的开销。
列表推导式的性能超过其函数式filter()
对应物——几乎是 2 倍,并且通常被认为更具 Python 风格。然而,易读性在这方面略显主观。有些人喜欢列表推导式直观和 Pythonic 的方式,而另一些人则偏爱使用filter()
函数,因为它清晰地传达了其功能和程序员的意图。
最后,让我们看一下reduce()
。这个内置函数通常用于需要在多个步骤中累积单一结果的情况。它还接受两个输入参数:(1)一个归约函数,和(2)一个可迭代对象。
让我们通过一个示例来使其功能更清晰。在这个例子中,我们希望计算一个整数列表的乘积:
>>> from functools import reduce
>>> integers = [1, 2, 3, 4, 5]
>>> reduce(lambda x, y: x * y, integers)
120
再次,我们使用一个 lambda 来定义我们的归约函数,这里是对整数列表进行简单的滚动乘法。这会执行以下计算:1 x 2 x 3 x 4 x 5 = 120。
使用列表推导式达到相同的目标这次有点棘手,需要一些额外的步骤,例如初始化变量和使用海象运算符:
>>> integers = [1, 2, 3, 4, 5]
>>> product = 1
>>> [product := product * num for num in numbers]
>>> product
120
虽然仍然可以通过列表推导式获得相同的结果,但这些额外的步骤显著降低了代码的可读性。
此外,现在还有多种低代码替代方法,例如math.prod()
:
>>> from math import prod
>>> integers = [1, 2, 3, 4, 5]
>>> prod(integers)
120
然而,在性能方面,这两者之间似乎没有重大区别:
>>> integers = list(range(1, 10001))
# using reduce
>>> %timeit -r 10 -n 100 reduce(lambda x, y: x * y, integers)
24.5 ms ± 299 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
# using math.prod
>>> from math import prod
>>> %timeit -r 10 -n 100 prod(integers)
23.8 ms ± 707 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
在 Python 中,reduce()
用于对列表中的值对进行滚动计算的使用在逐年减少,主要是因为有更高效和直观的替代方法,如math.prod()
。reduce()
和列表推导式在这里并没有提供一个清晰的语法,这使得读者很难快速理解代码。
PS:如果你仍然是reduce()
的频繁用户,我很想在评论中了解你的使用案例!
尽管在其他语言中不如其他语言那样普遍,map()
、filter()
以及偶尔使用的reduce()
仍然在基于 Python 的应用程序中使用。然而,列表推导式由于其更直观的语法被视为更具 Python 风格,并且在大多数情况下,可以替代map()
和filter()
函数,同时还带来明显的性能提升。
相比之下,reduce()
函数的特性使其不容易被列表推导式替代。然而,如上所述,它们可以被低代码替代方法如math.prod()
函数替代。
让我们联系一下!你可以在Twitter和LinkedIn找到我。
如果你喜欢支持我的写作,你可以通过Medium 会员来做到这一点,这将为你提供访问我所有故事以及 Medium 上其他成千上万作家的权限。
[## 通过我的推荐链接加入 Medium — Thomas A Dorfer
medium.com](https://medium.com/@thomasdorfer/membership?source=post_page-----1e2c9646fafe--------------------------------)