生成器表达式与列表理解


411

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

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

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

27
可能[exp for x in iter]只是糖list((exp for x in iter))吗?还是执行差异?
13年

1
它认为我有一个相关的问题,因此在使用yield时,可以仅使用函数中的生成器表达式,还是必须对函数使用yield来返回生成器对象?

28
@ b0fh对您的评论的回答很晚:在Python2中有一个微小的区别,循环变量将从列表理解中泄漏出来,而生成器表达式则不会泄漏。X = [x**2 for x in range(5)]; print x与比较Y = list(y**2 for y in range(5)); print y,第二个将给出错误。在Python3中,列表理解的确是生成器表达式的语法糖,list()正如您所期望的那样,因此,循环变量将不再泄漏出去
Bas Swinckels 2014年

12
我建议阅读PEP 0289。通过“该PEP引入生成器表达式作为列表推导和生成器的高性能,内存高效的概括”进行了总结。它还提供了有关何时使用它们的有用示例。
icc97 '16

5
@ icc97我参加聚会也晚了八年,PEP链接非常完美。感谢您轻松找到它!
eenblam '16

Answers:


283

John的答案很好(当您要迭代多次时,列表理解会更好)。但是,还应注意,如果要使用任何列表方法,都应使用列表。例如,以下代码将不起作用:

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

基本上,如果您要做的只是迭代一次,则使用生成器表达式。如果要存储和使用生成的结果,则最好使用列表理解功能。

由于性能是选择彼此的最常见原因,所以我的建议是不要担心它,而只选择一个即可。如果您发现程序运行速度太慢,则只有这样,您才应回去担心调整代码。


70
有时您必须使用生成器-例如,如果要使用带有yield的协作调度来编写协程。但是,如果您这样做的话,您可能就不会在问这个问题了;)
短暂的

12
我知道这已经很老了,但我认为值得注意的是,可以将生成器(以及任何可迭代的)添加到具有extend的列表中:a = [1, 2, 3] b = [4, 5, 6] a.extend(b)-现在a将为[1、2、3、4、5、6]。(您可以在评论中添加换行符吗?)
jarvisteve 2012年

12
@jarvisteve您的示例掩盖了您所说的话。这里还有一个要点。列表可以用生成器扩展,但是没有必要使它成为生成器。生成器不能使用列表扩展,并且生成器不是完全可迭代的。a = (x for x in range(0,10)), b = [1,2,3]例如。a.extend(b)引发异常。b.extend(a)将评估所有a,在这种情况下,首先使其成为生成器是没有意义的。
Slater Victoroff 2013年

4
@SlaterTyranus您是100%正确的,我为您的准确性投票。但是,我认为他的评论对OP的问题是有用的非回答,因为它将帮助那些在这里找到自己的人,因为他们在搜索引擎中输入了“结合列表理解的组合生成器”之类的内容。
rbp

1
使用生成器进行一次迭代的原因(例如,我对内存不足的关注优先于我对“一次获取”值的关注)可能在多次迭代时仍然适用吗?我想说这可能会使列表更有用,但是这是否足以超过内存问题是另外一回事。
罗伯·格兰特

181

遍历生成器表达式列表理解将执行相同的操作。但是,列表理解将首先在内存中创建整个列表,而生成器表达式将在运行中创建项目,因此您可以将其用于非常大的(也可以是无限的!)序列。


39
+1代表无限。无论您多么在乎性能,都无法使用列表来做到这一点。
Paul Draper 2013年

您可以使用推导方法创建无限生成器吗?
AnnanFay 2015年

5
@Annan仅当您已经可以访问另一个无限生成器时。例如,itertools.count(n)是从n开始的无穷整数序列,因此从开始(2 ** item for item in itertools.count(n))的幂的无穷序列也将是无限的序列。22 ** n
凯文

2
生成器在迭代之后从内存中删除项目。因此,如果您有大数据,则只想显示就可以了。它不是一个记忆猪。使用生成器时,将根据需要对项目进行处理。如果您想挂在列表上或再次遍历列表(以便存储项目),请使用列表理解。
j2emanue 2015年

102

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

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


2
这可能会有点偏离主题,但不幸的是“无法理解”……在这种情况下,“至高无上”意味着什么?我不是英语母语的人... :)
吉列尔莫·阿瑞斯

6
@GuillermoAres这是“搜索”最直接含义的直接结果:比其他任何事情都重要;最高。
Sнаđошƒаӽ

1
listsgenerator表达更快吗?通过阅读dF的答案,发现情况恰恰相反。
哈桑·拜格,2013年

1
最好说范围较小时列表理解更快,但是随着比例的增加,即时计算值变得更有价值-及时使用它们。这就是生成器表达式的作用。
凯尔(Kyle)

59

重要的是列表理解会创建一个新列表。生成器创建一个可迭代的对象,当您使用这些位时,它将动态“过滤”源材料。

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

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


46

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

例如:

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

9
用这种语言为您提供了一个提示,即生成器表达式应以这种方式使用。丢下括号!sum(x*2 for x in xrange(256))
u0b34a0f6ae

8
sortedreversed在任何可迭代的生成器表达式上正常工作。
marr75 2013年

1
如果你可以使用2.7及以上,这字典()的例子将看起来更好的字典理解(对于该PEP比则发电机表达PEP老,但需要较长的土地)
于尔根A.艾哈德

14

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

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

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


1
这应该是公认的答案。如果数据大于可用内存,则应始终使用生成器,尽管在内存中循环遍历列表可能会更快(但您没有足够的内存来这样做)。
Marek Marczak


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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.