什么时候不是使用python生成器的好时机?


83

这与您可以使用Python生成器函数做什么相反呢?:python生成器,生成器表达式和itertools模块是这些天我最喜欢的python功能。当设置操作链以对大量数据执行时,它们特别有用-我在处理DSV文件时经常使用它们。

那么什么时候不是使用生成器,生成器表达式或itertools函数的好时机?

  • 我什么时候应该更喜欢zip()itertools.izip(),或者
  • range()xrange()
  • [x for x in foo]结束了(x for x in foo)

显然,我们最终需要通常通过创建列表或使用非生成器循环对其进行迭代来将生成器“解析”为实际数据。有时我们只需要知道长度即可。这不是我要的

我们使用生成器,以便我们不会将新列表分配到内存中以存储临时数据。这对于大型数据集尤其有意义。小型数据集也有意义吗?是否存在明显的内存/ CPU权衡?

考虑到列表理解性能与map()和filter()的令人大开眼界的讨论,如果有人对此进行了分析,我尤其感兴趣。(alt链接


2
在这里提出了类似的问题,并进行了分析,发现在我的特定示例中, 列表对于长度可迭代的对象更快<5
亚历山大·麦克法兰

这回答了你的问题了吗?生成器表达式与列表理解
ggorlen

Answers:


57

在以下情况下,使用列表而不是生成器:

1)您需要访问数据的多个时间(即高速缓存的结果,而不是重新计算它们的):

for i in outer:           # used once, okay to be a generator or return a list
    for j in inner:       # used multiple times, reusing a list is better
         ...

2)您需要随机访问(或除正向顺序之外的任何访问):

for i in reversed(data): ...     # generators aren't reversible

s[i], s[j] = s[j], s[i]          # generators aren't indexable

3)您需要连接字符串(这需要对数据进行两次传递):

s = ''.join(data)                # lists are faster than generators in this use case

4)您使用的PyPy有时无法像正常函数调用和列表操作那样优化生成器代码。


对于#3,是否无法通过使用ireduce复制联接来避免两次通过?
2014年

谢谢!我不知道字符串连接行为。您能否提供或链接到为什么需要两次通过的说明?
大卫·艾克

5
@DavidEyk str.join进行了一次遍历以累加所有字符串片段的长度,因此它知道要为合并的最终结果分配多少内存。第二遍将字符串片段复制到新缓冲区中,以创建一个新字符串。参见hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…–
Raymond Hettinger

1
有趣的是,我经常使用生成器来加入sring。但是,我想知道,如果需要两次通过,它将如何工作?例如''.join('%s' % i for i in xrange(10))
bgusach 2014年

4
@ ikaros45如果要加入的输入不是列表,则必须做额外的工作才能为两次通过建立临时列表。大概是``data = data if isinstance(data,list)else list(data); n = sum(map(len,data)); 缓冲区= bytearray(n); ... <将片段复制到缓冲区>``''。
Raymond Hettinger 2014年

40

通常,当需要列表操作(例如len(),reversed()等)时,不要使用生成器。

有时候,您可能不需要懒惰的评估(例如,预先进行所有计算,以便释放资源)。在这种情况下,列表表达式可能会更好。


25
同样,预先进行所有计算可确保如果列表元素的计算引发异常,则它将在创建列表的位置抛出,而不是在随后遍历该列表的循环中抛出。如果需要在继续操作之前确保对整个列表进行无错误的处理,则生成器就不好了。
瑞安·汤普森

4
那是个很好的观点。半途而废地处理生成器,却让一切都爆炸了,这是非常令人沮丧的。可能有危险。
David Eyk

26

个人资料,个人资料,个人资料。

对代码进行性能分析是唯一了解您正在执行的操作是否有效的方法。

xrange,生成器等的大多数用法都超过了静态大小,小型数据集。只有当您访问大型数据集时,它才真正发挥作用。range()与xrange()基本上只是使代码看起来更难看一点,并且不会丢失任何东西,甚至可能有所收获。

个人资料,个人资料,个人资料。


1
的确如此。这些天之一,我将尝试进行经验比较。在那之前,我只是希望别人已经有了。:)
David Eyk

个人资料,个人资料,个人资料。我完全同意。个人资料,个人资料,个人资料。
Jeppe

17

你不应该偏向zipiziprangexrange,或者在发电机链表推导。在Python 3.0中range具有xrange类似的语义,并且zip具有izip样的语义。

实际上,列表理解更清晰,就像list(frob(x) for x in foo)那些您需要实际列表的时候一样。


3
@Steven我不同意,但我想知道您答案背后的原因是什么。为什么zip,range和list的理解永远都不应胜过相应的“懒惰”版本?
mhawke

正如他所说,因为邮编和范围的旧行为很快就会消失。

@Steven:好点。我忘记了3.0中的这些更改,这可能意味着在那里有人相信了它们的一般优势。回复:列表推导,它们通常更清晰(比扩展for循环还快!),但是人们可以轻松地编写难以理解的列表推导。
David Eyk,

9
我明白了您的意思,但我发现该[]表格具有足够的描述性(通常更简洁,更简洁)。但这只是一个品味问题。
David Eyk,

4
对于较小的数据大小,列表操作速度更快,但是当数据大小较小时,一切操作都会很快,因此,除非有特殊原因要使用列表,否则应始终偏爱生成器(出于此类原因,请参阅Ryan Ginstrom的回答)。
瑞安·汤普森

7

正如您提到的,“这对于大型数据集特别有意义”,我认为这回答了您的问题。

如果您在性能方面没有碰壁,您仍然可以坚持使用列表和标准功能。然后,当您遇到性能问题时,请进行切换。

如@ u0b34a0f6ae在评论中所述,但是,在一开始使用生成器可以使您更轻松地扩展到更大的数据集。


5
+1生成器使您的代码更适合大型数据集,而无需预期。
u0b34a0f6ae

6

关于性能:如果使用psyco,列表可以比生成器快很多。在下面的示例中,使用psyco.full()时,列表的速度几乎提高了50%

import psyco
import time
import cStringIO

def time_func(func):
    """The amount of time it requires func to run"""
    start = time.clock()
    func()
    return time.clock() - start

def fizzbuzz(num):
    """That algorithm we all know and love"""
    if not num % 3 and not num % 5:
        return "%d fizz buzz" % num
    elif not num % 3:
        return "%d fizz" % num
    elif not num % 5:
        return "%d buzz" % num
    return None

def with_list(num):
    """Try getting fizzbuzz with a list comprehension and range"""
    out = cStringIO.StringIO()
    for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
        print >> out, fibby
    return out.getvalue()

def with_genx(num):
    """Try getting fizzbuzz with generator expression and xrange"""
    out = cStringIO.StringIO()
    for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
        print >> out, fibby
    return out.getvalue()

def main():
    """
    Test speed of generator expressions versus list comprehensions,
    with and without psyco.
    """

    #our variables
    nums = [10000, 100000]
    funcs = [with_list, with_genx]

    #  try without psyco 1st
    print "without psyco"
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

    #  now with psyco
    print "with psyco"
    psyco.full()
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

if __name__ == "__main__":
    main()

结果:

without psyco
  number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds

  number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds

with psyco
  number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds

  number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds

1
这是因为psyco根本无法加快发电机的速度,因此,与发电机相比,psyco的缺点更多。好的答案,虽然。
史蒂芬·胡维格

4
另外,psyco现在几乎得不到维护。所有开发人员都花时间在PyPy的JIT上,据我所知,它会优化生成器。
努法尔·易卜拉欣

3

就性能而言,我想不出要在生成器上使用列表的任何时候。


all(True for _ in range(10 ** 8))all([True for _ in range(10 ** 8)])Python 3.8慢。我更喜欢列表而不是生成器
ggorlen

3

我从未发现过发电机会阻碍您尝试执行的操作的情况。但是,在很多情况下,使用生成器会比不使用生成器有所帮助。

例如:

sorted(xrange(5))

在以下方面未提供任何改进:

sorted(range(5))

4
range(5)由于结果列表已经排序,因此这两个方法都无法提供任何改进。
dan04 2014年

3

如果以后需要将值保留在其他位置并且集合的大小不太大,则应该使用列表推导。

例如:您正在创建一个列表,稍后您将在程序中循环多次。

在某种程度上,您可以将生成器视为迭代(循环)的替代品,而不是列表理解作为数据结构初始化的一种类型。如果要保留数据结构,请使用列表推导。


如果您只需要在流中进行有限的前瞻/后视,那么也许itertools.tee()可以为您提供帮助。但是通常来说,如果您希望进行一次以上的传递或随机访问某些中间数据,请对其进行列表/设置/操作。
贝尼·切尔尼亚夫斯基-帕斯金,
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.