【问题标题】:Generator expressions vs. list comprehensions生成器表达式与列表理解
【发布时间】:2023-02-02 08:05:12
【问题描述】:

什么时候应该使用生成器表达式,什么时候应该在 Python 中使用列表理解?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]

【问题讨论】:

  • [exp for x in iter] 可以只是 list((exp for x in iter)) 的糖吗?还是有执行差异?
  • 它认为我有一个相关的问题,所以当使用 yield 时,我们可以只使用函数中的生成器表达式,还是必须对函数使用 yield 来返回生成器对象?
  • @b0fh 对你的评论的回答很晚:在 Python2 中有一个微小的区别,循环变量会从列表理解中泄漏出来,而生成器表达式不会泄漏。比较X = [x**2 for x in range(5)]; print xY = list(y**2 for y in range(5)); print y,第二个会报错。在 Python3 中,列表理解确实是生成器表达式的语法糖,正如您所期望的那样,它被馈送到 list(),因此循环变量将为 no longer leak out
  • 我建议阅读PEP 0289。总结者“这个 PEP 引入了生成器表达式作为列表理解和生成器的高性能、内存高效的概括”.它还提供了有关何时使用它们的有用示例。
  • @icc97 我也迟到了八年,PEP 链接非常完美。感谢您使查找变得容易!

标签: python list-comprehension generator-expression


【解决方案1】:

John's answer 很好(当您想多次迭代某些内容时,列表理解会更好)。但是,同样值得注意的是,如果您想使用任何列表方法,您应该使用列表。例如,以下代码将不起作用:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

基本上,如果您所做的只是迭代一次,请使用生成器表达式。如果您想存储和使用生成的结果,那么您最好使用列表推导式。

由于性能是选择一个而不是另一个的最常见原因,我的建议是不要担心它而只选择一个;如果你发现你的程序运行得太慢,那么你应该回去担心调整你的代码。

【讨论】:

  • 有时候你使用生成器——例如,如果您使用 yield 编写协同调度的协程。但如果你这样做,你可能不会问这个问题 ;)
  • 我知道这是旧的,但我认为值得注意的是生成器(和任何可迭代的)可以添加到带有扩展的列表中:a = [1, 2, 3] b = [4, 5, 6] a.extend(b)——a 现在将是 [1、2、3、4、5、6]。 (你能在 cmets 中添加换行符吗??)
  • @jarvisteve 你的例子掩盖了你所说的话。这里也有一个很好的点。列表可以用生成器进行扩展,但是将它变成生成器是没有意义的。生成器不能用列表扩展,而且生成器不是可迭代的。 a = (x for x in range(0,10)), b = [1,2,3] 例如。 a.extend(b) 抛出异常。 b.extend(a) 将评估所有的 a,在这种情况下,首先将其作为生成器是没有意义的。
  • @SlaterTyranus 你 100% 正确,我为你的准确性点赞。尽管如此,我认为他的评论是对 OP 问题的一个有用的非回答,因为它会帮助那些发现自己在这里的人,因为他们在搜索引擎中输入了诸如“将生成器与列表理解相结合”之类的东西。
  • 使用生成器迭代一次的原因不是吗(例如我对内存不足的担忧压倒了我对一次“获取”一个值的担忧) 迭代多次时可能仍然适用?我会说这可能会使列表更有用,但是这是否足以克服内存问题是另一回事。
【解决方案2】:

遍历生成器表达式或者列表理解会做同样的事情。然而列表理解将首先在内存中创建整个列表,而生成器表达式将即时创建项目,因此您可以将它用于非常大(并且也是无限!)的序列。

【讨论】:

  • +1 无限。无论您多么不关心性能,您都无法使用列表来做到这一点。
  • 你能用推导式的方法创造无限的生成器吗?
  • @Annan 仅当您已经可以访问另一个无限生成器时。例如,itertools.count(n) 是从 n 开始的无限整数序列,因此 (2 ** item for item in itertools.count(n)) 将是从 2 ** n 开始的 2 的无限次幂序列。
  • 生成器在迭代后从内存中删除项目。因此,如果您有大数据,那么它的速度很快,您只想显示它,例如。它不是记忆猪。带有生成器的项目是“根据需要”处理的。如果你想挂在列表上或再次迭代它(所以存储项目)然后使用列表理解。
【解决方案3】:

当结果需要多次迭代,或者速度至关重要时,使用列表推导式。使用范围很大或无限大的生成器表达式。

有关详细信息,请参阅Generator expressions and list comprehensions

【讨论】:

  • 这可能有点离题,但不幸的是“不可搜索”......在这种情况下“至高无上”是什么意思?我不是以英语为母语的人... :)
  • @GuillermoAres 这是“谷歌搜索”最重要含义的直接结果:比什么都重要;最高。
  • 所以 listsgenerator 表达式更快?通过阅读 dF 的回答,发现情况恰恰相反。
  • 最好说当范围较小时列表推导速度更快,但随着规模的增加,动态计算值变得更有价值——正好赶上它们的使用。这就是生成器表达式的作用。
  • 好的,但是当它不适合这两个类别时,更喜欢(作为默认值)什么?
【解决方案4】:

重要的一点是列表理解创建了一个新列表。生成器创建一个可迭代对象,该对象将在您使用位时即时“过滤”源材料。

假设您有一个名为“hugefile.txt”的 2TB 日志文件,并且您想要以单词“ENTRY”开头的所有行的内容和长度。

所以你试着从写一个列表理解开始:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

这会吞噬整个文件,处理每一行,并将匹配的行存储在您的数组中。因此,该阵列最多可包含 2TB 的内容。那是很多 RAM,可能不适合您的目的。

因此,我们可以使用生成器对我们的内容应用“过滤器”。在我们开始迭代结果之前,实际上不会读取任何数据。

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

甚至还没有从我们的文件中读取一行。事实上,假设我们想进一步过滤我们的结果:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

仍然没有任何内容被读取,但我们现在已经指定了两个生成器,它们将按照我们的意愿对我们的数据进行操作。

让我们将过滤后的行写到另一个文件中:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

现在我们读取了输入文件。随着我们的 for 循环继续请求额外的行,long_entries 生成器从 entry_lines 生成器请求行,仅返回长度大于 80 个字符的行。然后,entry_lines 生成器从 logfile 迭代器请求行(按指示过滤),后者依次读取文件。

因此,不是以完全填充列表的形式将数据“推送”到输出函数,而是为输出函数提供一种仅在需要时“拉取”数据的方法。这在我们的例子中效率更高,但不够灵活。生成器是一种方式,一次传递;我们读取的日志文件中的数据会立即被丢弃,因此我们无法返回到上一行。另一方面,我们不必担心在处理完数据后如何保留数据。

【讨论】:

    【解决方案5】:

    生成器表达式的好处是它使用较少的内存,因为它不会立即构建整个列表。当列表作为中介时,最好使用生成器表达式,例如对结果求和,或根据结果创建字典。

    例如:

    sum(x*2 for x in xrange(256))
    
    dict( (k, some_func(k)) for k in some_list_of_keys )
    

    这样做的好处是列表没有完全生成,因此使用的内存很少(而且应该也更快)

    但是,当所需的最终产品是列表时,您应该使用列表理解。您不会使用生成器表达式保存任何内存,因为您需要生成的列表。您还可以获得能够使用任何列表函数(如排序或反转)的好处。

    例如:

    reversed( [x*2 for x in xrange(256)] )
    

    【讨论】:

    • 在语言中有一个提示,即生成器表达式应该以这种方式使用。去掉括号! sum(x*2 for x in xrange(256))
    • sortedreversed 可以很好地处理任何可迭代的生成器表达式。
    • 如果您可以使用 2.7 及更高版本,那么 dict() 示例作为字典理解看起来会更好(它的 PEP 比生成器表达式 PEP 更旧,但落地时间更长)
    • “也应该更快”部分与约翰米利金的回答相矛盾......
    【解决方案6】:

    从可变对象(如列表)创建生成器时,请注意生成器将在使用生成器时根据列表的状态进行评估,而不是在创建生成器时:

    >>> mylist = ["a", "b", "c"]
    >>> gen = (elem + "1" for elem in mylist)
    >>> mylist.clear()
    >>> for x in gen: print (x)
    # nothing
    

    如果您的列表有可能被修改(或该列表中的可变对象),但您需要创建生成器时的状态,则需要使用列表理解。

    【讨论】:

    • 这应该是公认的答案。如果您的数据大于可用内存,您应该始终使用生成器,尽管在内存中循环列表可能更快(但您没有足够的内存来这样做)。
    • 同样,修改底层列表gen 的迭代将导致不可预知的结果,just like 直接迭代列表。
    【解决方案7】:

    Python 3.7:

    列表理解更快。

    生成器的内存效率更高。

    正如所有其他人所说,如果您希望扩展无限数据,您最终将需要一个生成器。对于需要速度的相对静态的中小型作业,列表理解是最好的。

    【讨论】:

    • 这不是那么简单。列表组合仅在某些情况下更快。如果您正在使用 any 并且您预计会出现早期的 False 元素,则生成器可以显着改进列表理解。但是如果两者都用完了,那么列表组合通常会更快。你真的需要profile the application and see
    • 同意我是否有可能寻求/期待发电机提前停止。还同意需要对更复杂的项目进行更详尽的分析。我只是提供了这个简单的例子,感谢你的想法。
    【解决方案8】:

    有时你可以摆脱球座来自itertools 的函数,它为同一个生成器返回多个迭代器,这些迭代器可以独立使用。

    【讨论】:

      【解决方案9】:

      我正在使用Hadoop Mincemeat module。我认为这是一个很好的例子,需要注意:

      import mincemeat
      
      def mapfn(k,v):
          for w in v:
              yield 'sum',w
              #yield 'count',1
      
      
      def reducefn(k,v): 
          r1=sum(v)
          r2=len(v)
          print r2
          m=r1/r2
          std=0
          for i in range(r2):
             std+=pow(abs(v[i]-m),2)  
          res=pow((std/r2),0.5)
          return r1,r2,res
      

      在这里,生成器从文本文件(最大 15GB)中获取数字,并使用 Hadoop 的 map-reduce 对这些数字应用简单的数学运算。如果我没有使用 yield 函数,而是使用列表理解,计算总和和平均值会花费更长的时间(更不用说空间复杂度了)。

      Hadoop 是利用生成器的所有优点的一个很好的例子。

      【讨论】:

        【解决方案10】:

        内置 Python 函数的一些注意事项:

        如果需要 exploit the short-circuiting behaviour of any or all,请使用生成器表达式。这些函数旨在在已知答案时停止迭代,但是列表理解必须评估每个元素在调用函数之前。

        例如,如果我们有

        from time import sleep
        def long_calculation(value):
            sleep(1) # for simulation purposes
            return value == 1
        

        然后any([long_calculation(x) for x in range(10)]) 大约需要十秒钟,因为long_calculation 将被调用xany(long_calculation(x) for x in range(10)) 只需要大约两秒钟,因为long_calculation 只会用01 输入调用。

        anyall 迭代列表理解时,一旦知道答案,它们仍将停止检查truthiness 的元素(只要any 找到正确的结果,或all 找到错误的结果) ;然而,这通常是微不足道的与理解所做的实际工作相比。

        如果可以使用生成器表达式,它们当然会更节省内存。列表理解将是轻微地使用非短路 minmaxsum 更快(此处显示 max 的时间):

        $ python -m timeit "max(_ for _ in range(1))"
        500000 loops, best of 5: 476 nsec per loop
        $ python -m timeit "max([_ for _ in range(1)])"
        500000 loops, best of 5: 425 nsec per loop
        $ python -m timeit "max(_ for _ in range(100))"
        50000 loops, best of 5: 4.42 usec per loop
        $ python -m timeit "max([_ for _ in range(100)])"
        100000 loops, best of 5: 3.79 usec per loop
        $ python -m timeit "max(_ for _ in range(10000))"
        500 loops, best of 5: 468 usec per loop
        $ python -m timeit "max([_ for _ in range(10000)])"
        500 loops, best of 5: 442 usec per loop
        

        【讨论】:

          【解决方案11】:

          列表推导是急切的,而生成器是懒惰的。

          在列表理解中,所有对象都是立即创建的,创建和返回列表需要更长的时间。在生成器表达式中,对象的创建被延迟到 next() 提出请求。 next() 生成器对象被创建并立即返回。

          列表理解中的迭代速度更快,因为对象已经创建。

          如果你迭代列表理解和生成器表达式中的所有元素,时间性能是差不多的。即使生成器表达式立即返回生成器对象,它也不会创建所有元素。每次迭代一个新元素时,它都会创建并返回它。

          但是,如果您不遍历所有元素,生成器会更有效率。假设您需要创建一个包含数百万项的列表推导式,但您只使用了其中的 10 个。您仍然需要创建数百万个项目。您只是在浪费时间进行数百万次计算来创建数百万个项目以仅使用 10 个。或者如果您正在发出数百万个 api 请求但最终只使用了其中的 10 个。由于生成器表达式是惰性的,除非请求,否则它不会进行所有计算或 api 调用。在这种情况下,使用生成器表达式会更有效。

          在列表理解中,整个集合被加载到内存中。但是生成器表达式,一旦它在你的 next() 调用中返回一个值给你,它就完成了,它不再需要将它存储在内存中。只有一个项目被加载到内存中。如果您正在迭代磁盘中的一个大文件,如果文件太大,您可能会遇到内存问题。在这种情况下,使用生成器表达式更有效。

          【讨论】:

            【解决方案12】:

            我认为大多数答案都遗漏了一些东西。列表理解基本上创建一个列表并将其添加到堆栈中。在列表对象非常大的情况下,您的脚本进程将被终止。在这种情况下,生成器会更受欢迎,因为它的值不存储在内存中,而是存储为有状态函数。还有创作速度;列表理解比生成器理解慢

            简而言之; 当 obj 的大小不是太大时使用列表理解,否则使用生成器理解

            【讨论】:

              【解决方案13】:

              对于函数式编程,我们希望尽可能少地使用索引。出于这个原因,如果我们想在获取第一个元素切片后继续使用元素,islice() 是更好的选择,因为迭代器状态已保存。

              from itertools import islice
              
              def slice_and_continue(sequence):
                  ret = []
                  seq_i = iter(sequence) #create an iterator from the list
              
                  seq_slice = islice(seq_i,3) #take first 3 elements and print
                  for x in seq_slice: print(x),
              
                  for x in seq_i: print(x**2), #square the rest of the numbers
              
              slice_and_continue([1,2,3,4,5])
              

              输出:1 2 3 16 25

              【讨论】:

                猜你喜欢
                • 2013-12-30
                • 2021-11-12
                • 2016-10-04
                • 1970-01-01
                • 1970-01-01
                • 2020-11-08
                • 2018-10-13
                • 1970-01-01
                相关资源
                最近更新 更多