【问题标题】:List comprehension vs. lambda + filter列表理解与 lambda + 过滤器
【发布时间】:2011-03-02 02:29:58
【问题描述】:

我碰巧发现自己有一个基本的过滤需求:我有一个列表,我必须按项目的属性对其进行过滤。

我的代码如下所示:

my_list = [x for x in my_list if x.attribute == value]

但后来我想,这样写不是更好吗?

my_list = filter(lambda x: x.attribute == value, my_list)

它更具可读性,如果需要性能,可以取出 lambda 以获得一些东西。

问题是:使用第二种方式有什么注意事项吗?有什么性能差异吗?我是否完全错过了 Pythonic Way™,应该以另一种方式来实现(例如使用 itemgetter 而不是 lambda)?

【问题讨论】:

  • 一个更好的例子是你已经有一个很好命名的函数用作你的谓词。在这种情况下,我想更多的人会同意filter 更具可读性。当您有一个可以在 listcomp 中按原样使用的简单表达式,但必须包装在 lambda 中(或类似地由 partialoperator 函数等构造)以传递给 filter,那是 listcomps 获胜的时候。
  • 应该说至少在Python3中,filter的返回是一个过滤器生成器对象而不是一个列表。
  • 更具可读性?我想这是个人喜好问题,但对我来说,列表理解解决方案看起来像简单的英语:“对于 my_list 中的每个元素,只有当它的属性等于值时才使用它”(!?)。我想即使是非程序员也可能或多或少地尝试了解发生了什么。在第二个解决方案中......好吧......那个奇怪的“lamba”词是什么?同样,这可能是个人喜好问题,但我会一直选择列表理解解决方案,不管性能上的潜在微小差异,基本上只有研究人员感兴趣。

标签: python list functional-programming filter lambda


【解决方案1】:

很奇怪,不同的人有多少不同的美丽。我发现列表理解比filter+lambda 更清晰,但使用你觉得更容易的那个。

有两件事可能会减慢您对filter 的使用速度。

首先是函数调用开销:一旦您使用 Python 函数(无论是由 def 还是 lambda 创建的),过滤器很可能会比列表解析慢。几乎可以肯定,这还不够重要,在您对代码进行计时并发现它是一个瓶颈之前,您不应该过多考虑性能,但差异将会存在。

另一个可能的开销是强制 lambda 访问一个作用域变量 (value)。这比访问局部变量要慢,并且在 Python 2.x 中,列表推导仅访问局部变量。如果您使用的是 Python 3.x,则列表推导在单独的函数中运行,因此它也将通过闭包访问 value,这种差异将不适用。

要考虑的另一个选项是使用生成器而不是列表推导:

def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

然后在您的主代码(可读性真正重要的地方)中,您已将列表理解和过滤器替换为希望有意义的函数名称。

【讨论】:

  • +1 用于生成器。我在家里有一个演示文稿的链接,该演示文稿展示了发电机的神奇之处。您还可以通过将[] 更改为() 来用生成器表达式替换列表推导式。另外,我同意列表组合更漂亮。
  • 实际上,没有 - 过滤器更快。只需使用 stackoverflow.com/questions/5998245/… 之类的东西运行几个快速基准测试
  • @tnq177 这是 David Beasley 关于发电机的演讲 - dabeaz.com/generators
  • "...这是可读性真正重要的地方..."。抱歉,但可读性总是很重要,即使在(极少数)你哭着不得不放弃它的情况下也是如此。
【解决方案2】:

这在 Python 中是一个有点宗教性的问题。尽管 Guido considered removing map, filter and reduce from Python 3 引起了足够的反响,最终只有 reduce 从内置移动到 functools.reduce

我个人觉得列表推导更容易阅读。从表达式[i for i in list if i.attribute == value] 中可以更清楚地看出发生了什么,因为所有行为都在表面上,而不是在过滤器函数内部。

我不会太担心这两种方法之间的性能差异,因为它是微不足道的。如果它被证明是您的应用程序中不太可能出现的瓶颈,我真的只会优化它。

此外,由于 BDFL 希望 filter 从语言中消失,那么肯定会自动使列表推导更加 Pythonic ;-)

【讨论】:

  • 感谢 Guido 输入的链接,如果对我来说没有别的,这意味着我会尽量不再使用它们,这样我就不会养成习惯,也不会支持那个宗教的:)
  • 但是reduce是最复杂的简单工具! map 和 filter 很容易用理解替换!
  • 不知道 reduce 在 Python3 中被降级了。感谢您的洞察力! reduce() 在分布式计算中仍然很有帮助,比如 PySpark。我认为这是一个错误..
  • @Tagar 你仍然可以使用 reduce 你只需要从 functools 导入它
  • +1 表示“如果它被证明是您的应用程序中不太可能出现的瓶颈,我真的只会优化它。” – 这可能是题外话,但那里有太多不可读的代码,只是因为开发人员想要保护几微秒或 20 KB 的内存。除非边际较高的内存消耗或 2 或 5 微秒确实是一个问题,否则应该始终首选干净的代码。 (在这种情况下,使用filter 与使用列表推导式一样干净的代码。就我个人而言,我认为列表推导式更 Pythonic。)
【解决方案3】:

由于任何速度差异都必然是微乎其微的,因此是否使用过滤器或列表推导归结为一个品味问题。一般来说,我倾向于使用理解(这似乎与这里的大多数其他答案一致),但有一种情况我更喜欢filter

一个非常常见的用例是根据谓词 P(x) 提取一些可迭代 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)]

在这种情况下,我们希望 filter 反对后计算值。除了计算立方体两次的问题(想象一下更昂贵的计算),还有两次编写表达式的问题,违反了DRY 美学。在这种情况下,我会倾向于使用

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

【讨论】:

  • 你不会考虑通过另一个列表理解来使用素数吗?如[prime(i) for i in [x**3 for x in range(1000)]]
  • x*x*x 不能是素数,因为它有x^2x 作为一个因素,这个例子在数学上并没有真正的意义,但也许它仍然有帮助。 (也许我们可以找到更好的东西?)
  • 请注意,如果我们不想占用内存,我们可以在最后一个示例中使用生成器表达式:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
  • @MateenUlhaq 这可以优化为 prime_cubes = [1] 以节省内存和 CPU 周期;-)
  • @DennisKrupenik 或者更确切地说,[]
【解决方案4】:

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

【讨论】:

  • 对一个常见论点的后期评论:有时在 5 小时而不是 10 小时内运行分析会有所不同,如果这可以通过花 1 小时优化 python 代码来实现,它可以值得(特别是如果一个人熟悉 python 而不是更快的语言)。
  • 但更重要的是源代码在多大程度上减慢了我们阅读和理解它的速度!
  • 基本上,Pythonic 方式是一个秘密武器,当你想说我的想法比你的好时,你可以使用它。
【解决方案5】:

我想我只是在 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] 大致相同的时间完成。但是,在 3 中,相同的代码会留下包含过滤器对象的列表 c,而不是过滤后的列表。要在 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() 的输出以及原始列表。

【讨论】:

    【解决方案6】:

    一个重要的区别是列表解析将返回一个list,而过滤器返回一个filter,你不能像list那样操作它(即:调用len,它不适用于返回filter)。

    我自己的自学让我遇到了一些类似的问题。

    话虽如此,如果有办法从filter 得到list,有点像你在.NET 中做lst.Where(i => i.something()).ToList() 时所做的那样,我很想知道它。

    编辑:这是 Python 3 的情况,而不是 2(参见 cmets 中的讨论)。

    【讨论】:

    • filter 返回一个列表,我们可以在其上使用 len。至少在我的 Python 2.7.6 中。
    • 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)<class 'filter'>>>> type(lc)<class 'list'>
    • “如果有办法获得结果列表......我很想知道它”。只需在结果上调用list()list(filter(my_func, my_iterable))。当然,您可以将list 替换为set,或tuple,或其他任何需要可迭代的东西。但对于函数式程序员以外的任何人来说,使用列表推导而不是 filter 加上显式转换为 list 的情况会更强大。
    【解决方案7】:

    如果使用内置函数,通常filter 会稍微快一些。

    我希望在您的情况下列表理解会稍微快一些

    【讨论】:

    • 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 毫秒 不是吗?!
    • @sepdau,lambda 函数不是内置函数。列表理解在过去 4 年中有所改进 - 现在即使使用内置函数,差异也可以忽略不计
    【解决方案8】:

    我发现第二种方式更具可读性。它准确地告诉您意图是什么:过滤列表。
    PS:不要使用'list'作为变量名

    【讨论】:

      【解决方案9】:

      Filter 就是这样。它过滤掉列表的元素。您可以看到定义中提到的相同(在我之前提到的官方文档链接中)。然而,列表推导是在对前一个列表的 something 进行操作后生成一个新列表的东西。(过滤器和列表推导都会创建新列表,而不是代替旧列表执行操作。一个新列表这是一个类似于列表的东西,比如说,一种全新的数据类型。比如将整数转换为字符串等)

      在您的示例中,根据定义,最好使用过滤器而不是列表理解。但是,如果您想从列表元素中说 other_attribute,在您的示例中要作为新列表检索,那么您可以使用列表推导。

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

      这就是我对过滤器和列表理解的实际记忆。删除列表中的一些东西并保持其他元素不变,使用过滤器。在元素上自己使用一些逻辑并创建一个适合某种目的的淡化列表,使用列表理解。

      【讨论】:

      • 我很高兴知道投反对票的原因,这样我以后就不会再重复了。
      • 过滤器和列表理解的定义不是必需的,因为它们的含义没有被争论。列表推导应该只用于“新”列表,但没有被争论。
      • 我用定义说过滤器为您提供了具有相同元素的列表,这对于一个案例来说是正确的,但是通过列表理解,我们可以修改元素本身,比如将 int 转换为 str。但是点了:-)
      【解决方案10】:

      当我需要在列表理解之后过滤某些内容时,我会使用以下一小段内容。只是过滤器、lambda 和列表的组合(也称为猫的忠诚度和狗的清洁度)。

      在这种情况下,我正在读取一个文件,删除空行,注释掉的行,以及一行注释之后的任何内容:

      # 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])]
      

      【讨论】:

      • 这确实在很少的代码中实现了很多。我认为一行中的逻辑可能有点过多,难以理解,但可读性才是最重要的。
      • 你可以写成file_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
      【解决方案11】:

      我花了一些时间来熟悉higher order functions filtermap。所以我习惯了它们,我真的很喜欢filter,因为它很明显它会通过保持真实的东西来过滤,而且我对知道一些functional programming 术语感到很酷。

      然后我读了这段话(Fluent Python Book):

      地图和过滤器功能仍然是内置的 在 Python 3 中,但是自从引入了列表推导和生成器 ex- 压力,它们并不那么重要。 listcomp 或 genexp 完成 map 和 过滤器组合,但更具可读性。

      现在我想,如果您可以使用已经广泛传播的习语(如列表推导式)来实现它,为什么还要为 filter / map 的概念而烦恼。此外mapsfilters 是一种功能。在这种情况下,我更喜欢使用Anonymous functions lambdas。

      最后,为了进行测试,我对这两种方法(maplistComp)都进行了计时,但我没有看到任何相关的速度差异可以证明对此进行争论是合理的。

      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
      

      【讨论】:

        【解决方案12】:

        除了公认的答案之外,还有一个极端情况是您应该使用过滤器而不是列表推导式。如果列表是不可散列的,则您无法使用列表理解直接处理它。一个真实的例子是,如果您使用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'

        【讨论】:

        • 所有列表都是不可散列的 >>> 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]
        • "如果列表是不可散列的,你不能直接用列表理解来处理它。"这不是真的,而且 所有 列表无论如何都是不可散列的。
        【解决方案13】:

        奇怪的是,在 Python 3 上,我发现过滤器的执行速度比列表推导式更快。

        我一直认为列表推导会更高效。 就像是: [如果 name 不是 None,则为 brand_names_db 中的名称名称] 生成的字节码稍微好一点。

        >>> 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
        

        【讨论】:

        • 无效比较。首先,您没有将 lambda 函数传递给过滤器版本,这使其默认为标识函数。在列表推导中定义if not None 时,您正在定义一个lambda 函数(注意MAKE_FUNCTION 语句)。其次,结果不同,因为列表理解版本将仅删除 None 值,而过滤器版本将删除所有“虚假”值。话虽如此,微基准测试的全部目的是无用的。那是一百万次迭代,乘以 1k 个项目!差别可以忽略不计
        • list(filter(None, seq)) 等于 [i for i in seq if i] 而不是 i is not Nonedocs.python.org/3/library/functions.html#filter
        【解决方案14】:

        总结其他答案

        查看答案,我们已经看到很多来回,列表理解或过滤器是否可能更快,或者是否关心这样的问题是否重要或 pythonic。最后,大多数情况下的答案都是:视情况而定。

        我在优化代码时偶然发现了这个问题,其中这个确切的问题(尽管结合了 in 表达式,而不是 ==)非常相关 - filter + lambda 表达式占了三分之一我的计算时间(数分钟)。

        我的情况

        就我而言,列表理解要快得多(速度的两倍)。但我怀疑这会因过滤器表达式以及使用的 Python 解释器而有很大差异。

        自己测试一下

        这是一个简单的代码sn-p,应该很容易适应。如果您对其进行分析(大多数 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,请尝试使用此方法(从我的代码库中提取,因此有点复杂)。此代码 sn-p 将为您创建一个配置文件,您可以使用例如轻松可视化。 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))
        

        【讨论】:

          【解决方案15】:

          你的问题既简单又有趣。它只是展示了 Python 作为一种编程语言是多么灵活。可以使用任何逻辑,根据自己的才能和理解编写程序。只要我们得到答案就可以了。

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2022-11-29
            • 2020-08-25
            • 1970-01-01
            相关资源
            最近更新 更多