列表推导式 vs lambda + filter

1104

我有一个列表,想要根据项的属性进行过滤。

以下哪个更好(可读性、性能、其他原因)?

xs = [x for x in xs if x.attribute == value]
xs = filter(lambda x: x.attribute == value, xs)

31
一个更好的例子是,当你已经有了一个很好命名的函数可以用作谓词时。在这种情况下,我认为更多人会同意 filter 更易读。当你有一个简单的表达式可以直接在列表推导式中使用,但必须将其包装在 lambda 函数(或类似地由 partialoperator 函数构建等)中才能传递给 filter 时,此时列表推导式胜出。 - abarnert
19
应该说,在Python3中,filter函数的返回值是一个过滤器生成器对象,而不是一个列表。 - Matteo Ferla
6
更易读?我想这是个人口味的问题,但对我来说,列表推导式的解决方案看起来就像是平常英语:“对于我的列表中的每个元素,只有在它的属性等于某个值的情况下才取它”。我猜即使非程序员也可能会试图理解正在发生的事情,或多或少。而在第二种解决方案中……那是一种奇怪的“lambda”单词,首先是什么意思?再次,这可能是个人口味的问题,但无论潜在的微小性能差异只是研究者感兴趣的东西,我都会始终选择列表推导式的解决方案。 - Sal Borrelli
1
filter 的具体实现并不是很易读(这并不奇怪,因为 Python 并不是真正的函数式编程语言)。在语言开发历史中,交换参数顺序可能是更好的选择,即 filter(xs, lambda: x: ...) 将从左到右阅读,就像“过滤 xs 以仅保留满足条件的值”一样。可以说,推导式应该被认为更易读,因为它是从左到右可理解的(你看我做了什么吗?),并且基于 Python 的非 FP 语言属性和 filter 的不太易读的实现等方面更符合“Pythonic”的风格。 - Ezekiel Victor
17个回答

711
奇怪的是,美丽在不同的人眼中变化巨大。我认为列表推导比filter+lambda更清晰,但请使用您认为更容易的方式。
有两件事可能会减慢您使用filter的速度。
第一个是函数调用开销:一旦您使用Python函数(无论是通过def还是lambda创建的),filter很可能比列表推导要慢。这几乎肯定不足以影响性能,并且在发现代码成为瓶颈之前,您不应该过多考虑性能,但是差异将存在。
另一个可能适用的开销是强制lambda访问作用域变量(value)。这比访问局部变量慢,在Python 2.x中,列表推导只访问局部变量。如果您使用的是Python 3.x,则列表推导运行在单独的函数中,因此它也将通过闭包访问value ,并且该差异将不适用。
另一个要考虑的选项是使用生成器而不是列表推导:
def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

然后在你的主要代码中(这里可读性真正重要),你用一个有意义的函数名替换了列表解析和过滤器。


82
为发电机点赞。我在家里有一个链接,展示了发电机的惊人之处。只需将列表推导式中的 [] 替换为生成器表达式中的 (),就可以将其替换为生成器表达式。同时,我同意列表推导式更加美观。 - Wayne Werner
2
实际上,不是 - 过滤器更快。只需使用类似于https://dev59.com/mm025IYBdhLWcg3wclq-的东西运行几个快速基准测试即可。 - skqr
4
最好使用 timeit 进行基准测试,但请给出一个例子,展示当使用 Python 回调函数时,你发现 filter 更快的情况。 - Duncan
13
@tnq177 这是David Beasley有关生成器的演示 - http://www.dabeaz.com/generators/ - Wayne Werner
2
"...这就是可读性真正重要的地方..."。抱歉,即使在(很少见的)情况下,当你不得不放弃可读性时,可读性始终很重要。 - Victor Schröder
显示剩余4条评论

290
这是Python中一个有点宗教色彩的问题。尽管Guido曾考虑从Python 3中删除map、filter和reduce, 但是由于反对声浪够大,最终只将reduce从内置函数移动到了functools.reduce
就个人而言,我觉得列表推导式更易读。从表达式[i for i in list if i.attribute == value]可以更清晰地看出所有行为都在表面上发生,而不是在filter函数内部。
我不会过分担心两种方法之间的性能差异,因为它是微不足道的。除非在您的应用程序中证明这是瓶颈,否则我真的只会优化这一点。
此外,既然BDFL希望从语言中删除filter,那么无疑列表推导式更符合Python风格。;)

4
谢谢提供 Guido 的建议链接,对我来说至少意味着我将尽量不再使用它们,这样我就不会养成习惯,也不会支持那个宗教 :) - dashesy
2
但是使用简单工具来完成reduce是最复杂的!map和filter很容易用推导式替代! - njzk2
11
不知道reduce在Python3中被降级了,谢谢你的见解!reduce()在分布式计算中仍然非常有用,比如PySpark。我认为那是个错误。 - Tagar
2
@Tagar,你仍然可以使用reduce,只需要从functools导入它即可。 - icc97
2
+1 表示“只有在它被证明是应用程序的瓶颈时,才会真正优化它,这是不太可能的。” - 这可能与主题无关,但是由于开发人员想要节省几微秒或 20 KB 的内存,因此存在大量难以阅读的代码。除非边际更高的内存消耗或 2 或 5 微秒真正成为问题,否则应始终首选清晰的代码。(在这种情况下,使用 filter 和使用列表推导式一样干净。就个人而言,我认为列表推导式更具 Python 风格。) - Thomas

91

由于任何速度差异都很小,因此使用过滤器或列表推导式取决于个人喜好。总的来说,我倾向于使用推导式(这似乎与其他大多数答案一致),但有一种情况我更喜欢使用filter

一个非常常见的用例是从一些可迭代对象X中提取满足谓词P(x)的值:

[x for x in X if P(x)]

但有时您希望先对值应用某些函数:

[f(x) for x in X if P(f(x))]
作为一个具体的例子,考虑。
primes_cubed = [x*x*x for x in range(1000) if prime(x)]

我认为这比使用filter看起来略好一些。但是现在考虑

prime_cubes = [x*x*x for x in range(1000) if prime(x*x*x)]

在这种情况下,我们希望针对后计算出的值进行过滤。除了计算立方体两次的问题(想象一下更昂贵的计算),还存在写表达式两次的问题,违反了DRY美学原则。在这种情况下,我倾向于使用

prime_cubes = filter(prime, [x*x*x for x in range(1000)])

11
你考虑过使用另一个列表推导式来计算质数吗?例如 [prime(i) for i in [x**3 for x in range(1000)]] - viki.omega9
37
“xxx”不能是质数,因为它有“x^2”和“x”作为因数,这个例子在数学上并不真正有意义,但也许仍然有帮助(也许我们可以找到更好的例子?) - Zelphir Kaltstahl
6
请注意,如果我们不想占用内存,可以使用生成器表达式来代替最后一个例子:prime_cubes = filter(prime, (x*x*x for x in range(1000)))。生成器表达式与列表推导式很相似,但是它们以惰性方式生成元素,只在需要时才生成下一个值,因此不会占用大量内存。 - Mateen Ulhaq
7
这可以优化为 prime_cubes = [1],以节省内存和CPU循环;-) - Dennis Krupenik
12
好的,我会尽力进行翻译。以下是需要翻译的内容:@DennisKrupenik 或者说 [] - Mateen Ulhaq
显示剩余6条评论

35

虽然 filter 可能是“更快的方式”,但 “Pythonic 方式” 是不关心这些事情,除非性能绝对关键(在这种情况下,您不会使用 Python!)。


16
针对一个经常出现的论点进行晚期评论:有时候使得分析在5小时内运行而不是10小时内会有所不同,如果这可以通过优化Python代码花费一小时来实现,那么这可能是值得的(特别是如果你对Python很熟悉而对更快的语言不熟悉)。 - bli
2
但更重要的是源代码会让我们在尝试阅读和理解它时慢下来的程度! - thoni56
3
基本上,Pythonic的方式是一种秘密武器,当你想要表达“我的想法比你的更好”时可以使用它。 - user1663023
filter 不再更快了:请参见 https://dev59.com/qHA75IYBdhLWcg3w8-HZ#74432106 - Michael Dorner

33

我认为我应该补充一下,在Python 3中,filter()实际上是一个迭代器对象,因此您需要将过滤方法调用传递给 list() 来构建过滤后的列表。所以在Python 2中:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = filter(lambda num: num % 2 == 0, lst_a)

列表b和c具有相同的值,并且在完成时间上与filter()等效 [x for x in y if z] 差不多。然而,在Python 3中,这段代码将使列表c包含一个过滤器对象,而不是过滤后的列表。要想在Python 3中得到相同的结果:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = list(filter(lambda num: num %2 == 0, lst_a))
问题在于list()需要接收一个可迭代对象作为参数,并从该参数创建一个新的列表。这意味着在Python 3中使用filter()方法要比[x for x in y if z]方法慢最多一倍,因为你需要遍历filter()的输出以及原始列表。

18
一个重要的区别是列表推导式将返回一个list,而过滤器将返回一个filter,您无法像操作list一样操纵它(例如:调用len,这在filter返回时不起作用)。
我的自学带给我一些类似的问题。
话虽如此,如果有一种方法可以从filter中获取结果的list,就像在.NET中执行lst.Where(i => i.something()).ToList()一样,我很想知道它。
编辑:这适用于Python 3,而不是2(请参见评论讨论)。

4
筛选器返回一个列表,我们可以在其上使用len。至少在我的Python 2.7.6版本中是这样的。 - thiruvenkadam
9
在Python 3中不是这种情况。`a = [1, 2, 3, 4, 5, 6, 7, 8]` `f = filter(lambda x: x % 2 == 0, a)` `lc = [i for i in a if i % 2 == 0]` `>>> type(f)` `` `>>> type(lc)` `` - Adeynack
5
如果有一种方法可以得到结果列表...我很好奇想要知道它。只需在结果上调用"list()"函数即可:"list(filter(my_func, my_iterable))"。当然,您也可以将"list()"替换为"set()"、"tuple()"或者任何其他接受一个可迭代对象的函数。但对于非函数式编程人员而言,使用列表推导式比起使用"filter"加显式转换为"list"的方式更为明智。 - Steve Jessop

10

我认为第二种方式更易读。它明确地告诉你意图是过滤列表。
PS: 不要使用“list”作为变量名。


9

Filter就是这样。它过滤掉列表中的元素。您可以在我之前提到的官方文档链接中看到相同的定义。而列表推导式则是在先前的列表上执行某些操作后产生新列表的东西。(过滤和列表推导都创建一个新列表,而不是在旧列表的原地执行操作。这里的新列表就像一个带有全新数据类型的列表。例如将整数转换为字符串等)

在您的示例中,根据定义使用过滤器比使用列表推导更好。然而,如果您想要从列表元素中检索其他属性作为新列表,在您的示例中,则可以使用列表推导。

return [item.other_attribute for item in my_list if item.attribute==value]

这是我对于过滤器和列表推导式的实际记忆方式。如果要在列表中删除一些元素并保留其他元素不变,请使用过滤器。如果需要对元素进行某些逻辑操作并创建一个适用于某些目的的简化列表,请使用列表推导式。

2
我很乐意知道被投票否决的原因,这样我将来就不会再犯同样的错误了。 - thiruvenkadam
1
不必要解释filter和list comprehension的定义,因为它们的含义并未受到争议。虽然提出了列表推导式仅用于“新”列表,但并未进行论证。 - Agos
我使用定义来说明filter函数会返回一个列表,其中包含与某个条件为真的元素相同的元素,但是使用列表推导式我们可以修改元素本身,例如将int转换为str。但是我明白您的意思 :-) - thiruvenkadam

9
通常情况下,如果使用内置函数,filter 会稍微快一些。
在您的情况下,我预计列表推导式会稍微快一些。

1
python -m timeit 'filter(lambda x: x in [1,2,3,4,5], range(10000000))' 10次循环,3次中的最佳结果:每个循环耗时1.44秒python -m timeit '[x for x in range(10000000) if x in [1,2,3,4,5]]' 10次循环,3次中的最佳结果:每个循环耗时860毫秒真的吗? - giaosudau
@sepdau,lambda函数不是内置函数。列表推导式在过去的4年中得到了改进 - 现在即使使用内置函数,差异也可以忽略不计。 - John La Rooy

7

当我需要在列表推导完成后对某些内容进行过滤时,我常常使用以下简短代码片段。这是一个filter、lambda和list的结合(也被称为猫的忠诚度和狗的清洁度)。

在本例中,我正在读取一个文件,并剔除空行、注释行以及注释后面的任何内容:

# Throw out blank lines and comments
with open('file.txt', 'r') as lines:        
    # From the inside out:
    #    [s.partition('#')[0].strip() for s in lines]... Throws out comments
    #   filter(lambda x: x!= '', [s.part... Filters out blank lines
    #  y for y in filter... Converts filter object to list
    file_contents = [y for y in filter(lambda x: x != '', [s.partition('#')[0].strip() for s in lines])]

这确实在很少的代码中实现了很多。我认为一行中可能包含了太多的逻辑,不容易理解,但可读性才是最重要的。 - Zelphir Kaltstahl
1
你可以这样写:file_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines))) - Steve Jessop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接