列表推导式 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个回答

5
我花了一些时间熟悉高阶函数中的filter和map。当我逐渐习惯它们并且实际上喜欢filter时,因为它清晰地表明它通过保持为truthy的内容进行过滤,我感到很酷因为我知道一些函数式编程术语。
然后我读到了这段话(《流畅的Python》书):
“在Python 3中,map和filter函数仍然是内置函数,但自列表推导和生成器表达式引入以来,它们已经不那么重要了。一个列表推导符或者一个生成器表达式可完成map和filter的工作,但更易读。”
现在我想,如果您可以使用已经广泛传播的列表理解等成语来实现它,为什么还要费心去理解filter/map的概念呢?此外,map和filter算是函数。在这种情况下,我更喜欢使用匿名函数lambda。
最后,为了测试它,我对两种方法(map和listComp)进行了计时,并没有看到任何有关速度的显著差异,这证明不需要对此做出争论。
from timeit import Timer

timeMap = Timer(lambda: list(map(lambda x: x*x, range(10**7))))
print(timeMap.timeit(number=100))

timeListComp = Timer(lambda:[(lambda x: x*x) for x in range(10**7)])
print(timeListComp.timeit(number=100))

#Map:                 166.95695265199174
#List Comprehension   177.97208347299602

5

就性能而言,这要看情况。

filter 不会返回列表,而是一个迭代器。如果你需要“立即”过滤并转换成列表,对于非常大的列表(>1M),使用列表推导比使用 filter 和列表转换慢约40%。对于100K个元素以下的列表,几乎没有区别;从600K开始就会出现差异。

如果不转换为列表,则 filter 几乎是瞬间完成的。

更多信息请见:https://blog.finxter.com/python-lists-filter-vs-list-comprehension-which-is-faster/


4
除了已经被接受的答案,还有一种情况需要使用过滤器而不是列表推导式。如果列表是不可哈希的,则无法直接使用列表推导式处理它。一个现实世界的例子是,如果您使用 pyodbc 从数据库读取结果,则 cursorfetchAll() 结果是一个不可哈希的列表。在这种情况下,应该使用过滤器来直接操作返回的结果:
cursor.execute("SELECT * FROM TABLE1;")
data_from_db = cursor.fetchall()
processed_data = filter(lambda s: 'abc' in s.field1 or s.StartTime >= start_date_time, data_from_db) 

如果你在这里使用列表推导式,你将会得到以下错误:
TypeError: unhashable type: 'list'

2
所有列表都是不可哈希的。 >>> hash(list()) # TypeError: unhashable type: 'list' 其次,这个可以正常工作:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time] - Thomas Grainger
2
如果列表是不可哈希的,则无法直接使用列表推导式处理它。这并不正确,所有列表都是不可哈希的。 - juanpa.arrivillaga

4
我得出的结论是:与filter相比,使用列表推导式更易读、更符合Python风格且更快(对于Python 3.11,请参见附带的基准测试数据,也可参见)。请记住,filter返回的是迭代器而不是列表。
python3 -m timeit '[x for x in range(10000000) if x % 2 == 0]'            

1个循环,5次中的最佳表现:每个循环270毫秒

python3 -m timeit 'list(filter(lambda x: x % 2 == 0, range(10000000)))'

1个循环,5次中的最佳结果:每个循环432毫秒


2

有趣的是,在Python 3上,我发现过滤器比列表推导式执行得更快。

我一直以为列表推导式会更高效,比如:

[name for name in brand_names_db if name is not None]

生成的字节码会更好一些。

>>> def f1(seq):
...     return list(filter(None, seq))
>>> def f2(seq):
...     return [i for i in seq if i is not None]
>>> disassemble(f1.__code__)
2         0 LOAD_GLOBAL              0 (list)
          2 LOAD_GLOBAL              1 (filter)
          4 LOAD_CONST               0 (None)
          6 LOAD_FAST                0 (seq)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION            1
         12 RETURN_VALUE
>>> disassemble(f2.__code__)
2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10cfcaa50, file "<stdin>", line 2>)
          2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_FAST                0 (seq)
          8 GET_ITER
         10 CALL_FUNCTION            1
         12 RETURN_VALUE

但事实上它们更慢:

   >>> timeit(stmt="f1(range(1000))", setup="from __main__ import f1,f2")
   21.177661532000116
   >>> timeit(stmt="f2(range(1000))", setup="from __main__ import f1,f2")
   42.233950221000214

11
无效的比较。首先,在filter版本中,您没有传递lambda函数,这会使其默认为恒等函数。在列表推导中定义if not None时,您正在定义一个lambda函数(请注意MAKE_FUNCTION语句)。其次,两个版本的结果是不同的,因为列表推导版本只会删除None值,而filter版本将删除所有“假值”。话虽如此,微基准测试的整个目的是无意义的。这些是一百万个迭代,每个迭代有1k个项目!差异是微不足道的。 - Victor Schröder
list(filter(None, seq)) 等价于 [i for i in seq if i],而不是 i 不为 None。https://docs.python.org/3/library/functions.html#filter - Sole Sensei

1

总结其他答案

浏览了其他答案后,我们看到了很多争论,无论是列表推导式还是过滤器可能更快,或者是否重要或符合Pythonic关心这样的问题。最终,答案像大多数时候一样:取决于情况。

我在优化代码时偶然遇到了这个问题(虽然与in表达式相结合,而不是==),这非常相关-filter+ lambda表达式占用了我计算时间的三分之一(多分钟)。

我的情况

在我的情况下,列表推导式要快得多(速度加倍)。但我怀疑这也强烈取决于过滤器表达式以及所使用的Python解释器。

为自己测试

这是一个简单的代码片段,应该很容易适应。如果您对其进行分析(大多数IDE都可以轻松完成此操作),您将能够轻松地为您的特定情况决定哪个选项更好:

whitelist = set(range(0, 100000000, 27))

input_list = list(range(0, 100000000))

proximal_list = list(filter(
        lambda x: x in whitelist,
        input_list
    ))

proximal_list2 = [x for x in input_list if x in whitelist]

print(len(proximal_list))
print(len(proximal_list2))

如果您没有可以轻松进行性能分析的集成开发环境(IDE),则可以尝试以下方法(从我的代码库中提取,因此有些复杂)。这段代码片段将为您创建一个性能分析文件,您可以使用例如snakeviz轻松可视化它:

import cProfile
from time import time


class BlockProfile:
    def __init__(self, profile_path):
        self.profile_path = profile_path
        self.profiler = None
        self.start_time = None

    def __enter__(self):
        self.profiler = cProfile.Profile()
        self.start_time = time()
        self.profiler.enable()

    def __exit__(self, *args):
        self.profiler.disable()
        exec_time = int((time() - self.start_time) * 1000)
        self.profiler.dump_stats(self.profile_path)


whitelist = set(range(0, 100000000, 27))
input_list = list(range(0, 100000000))

with BlockProfile("/path/to/create/profile/in/profile.pstat"):
    proximal_list = list(filter(
            lambda x: x in whitelist,
            input_list
        ))

    proximal_list2 = [x for x in input_list if x in whitelist]

print(len(proximal_list))
print(len(proximal_list2))

0

你的问题非常简单却有趣。它展示了Python作为一种编程语言的灵活性,一个人可以使用任何逻辑并根据他们的才能和理解编写程序。只要我们得到答案就可以。

在你的情况下,这只是一个简单的过滤方法,两种方法都可以实现,但我更喜欢第一种my_list = [x for x in my_list if x.attribute == value],因为它看起来简单,不需要特殊语法。任何人都可以理解这个命令,并进行必要的更改。 (虽然第二种方法也很简单,但对于初学者级别的程序员来说,它仍然比第一种方法更复杂)


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