478

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

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

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

12 回答 12

328

约翰的回答很好(当你想多次迭代某些东西时,列表理解会更好)。但是,还值得注意的是,如果您想使用任何列表方法,则应该使用列表。例如,以下代码将不起作用:

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

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

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

于 2008-09-06T20:54:08.063 回答
212

迭代生成器表达式列表推导将做同样的事情。然而,列表推导式将首先在内存中创建整个列表,而生成器表达式将动态创建项目,因此您可以将它用于非常大的(也是无限的!)序列。

于 2008-09-06T20:11:17.243 回答
125

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

有关更多信息,请参阅生成器表达式和列表推导。

于 2008-09-06T20:10:59.460 回答
66

重要的一点是列表推导创建了一个新列表。生成器创建一个可迭代对象,当您使用这些位时,它将即时“过滤”源材料。

假设您有一个名为“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迭代器又读取文件。

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

于 2014-04-04T09:14:57.693 回答
50

生成器表达式的好处是它使用更少的内存,因为它不会一次构建整个列表。当列表是中介时,最好使用生成器表达式,例如对结果求和,或从结果中创建一个 dict。

例如:

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)] )
于 2008-10-10T01:42:30.253 回答
18

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

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

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

于 2016-03-12T22:21:00.660 回答
6

蟒蛇 3.7:

列表推导更快。

在此处输入图像描述

生成器的内存效率更高。 在此处输入图像描述

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

于 2020-07-03T06:37:33.027 回答
4

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

于 2008-09-10T00:58:03.740 回答
4

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

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 是利用生成器的所有优点的一个很好的例子。

于 2016-01-04T20:31:50.893 回答
1

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

在列表推导中,所有对象都是立即创建的,创建和返回列表需要更长的时间。在生成器表达式中,对象创建被延迟到由next(). 一旦next()生成器对象被创建并立即返回。

列表推导中的迭代更快,因为已经创建了对象。

如果迭代列表推导和生成器表达式中的所有元素,时间性能大致相同。即使生成器表达式立即返回生成器对象,它也不会创建所有元素。每次迭代一个新元素时,它都会创建并返回它。

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

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

于 2021-01-14T18:23:57.023 回答
1

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

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

于 2021-12-20T12:09:37.647 回答
0

对于函数式编程,我们希望尽可能少地使用索引。出于这个原因,如果我们想在获取第一个元素切片后继续使用元素,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

于 2022-01-17T01:39:00.857 回答