【问题标题】:When is not a good time to use python generators?什么时候不适合使用 python 生成器?
【发布时间】:2010-09-19 17:39:07
【问题描述】:

这与What can you use Python generator functions for? 正好相反:python 生成器、生成器表达式和itertools 模块是我最近最喜欢的python 的一些特性。它们在设置操作链以对大量数据执行时特别有用——我经常在处理 DSV 文件时使用它们。

那么什么时候适合使用生成器、生成器表达式或itertools 函数?

  • 什么时候我应该更喜欢zip() 而不是itertools.izip(),或者
  • range() 超过 xrange(),或
  • [x for x in foo] 超过 (x for x in foo)?

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

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

鉴于list comprehension performance vs. map() and filter() 的令人大开眼界的讨论,如果有人对此进行了一些分析,我特别感兴趣。 (alt link)

【问题讨论】:

标签: python optimization iterator generator


【解决方案1】:

您永远不应该偏爱zip 而不是iziprange 而不是xrange,或者列表推导式而不是生成器推导式。在 Python 3.0 中,range 具有类似 xrange 的语义,zip 具有类似 izip 的语义。

在您需要实际列表的时候,列表推导式实际上更清晰,例如 list(frob(x) for x in foo)

【讨论】:

  • @Steven 我不反对,但我想知道你的答案背后的原因是什么。为什么 zip、range 和列表推导永远不会比相应的“懒惰”版本更受欢迎??
  • 因为正如他所说,旧的 zip 和 range 行为很快就会消失。
  • @Steven:好点子。我忘记了 3.0 中的这些变化,这可能意味着那里的某个人确信他们的总体优势。回复:列表推导,它们通常更清晰(并且比扩展的for 循环更快!),但可以轻松编写难以理解的列表推导。
  • 我明白你的意思,但我发现[] 形式的描述性已经足够了(通常更简洁,更简洁)。但这只是口味问题。
  • 对于小数据大小,列表操作更快,但当数据大小较小时,everything 速度很快,因此除非您有特定使用原因,否则您应该始终首选生成器列表(出于这些原因,请参阅 Ryan Ginstrom 的回答)。
【解决方案2】:

一般来说,在需要列表操作时不要使用生成器,比如 len()、reversed() 等。

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

【讨论】:

  • 另外,预先进行所有计算可以确保如果列表元素的计算抛出异常,它将在列表创建的点被抛出,而不是在随后迭代它的循环中。如果您需要确保在继续之前对整个列表进行无错误处理,那么生成器就不好了。
  • 这很好。处理一个生成器到一半,结果一切都爆炸了,这是非常令人沮丧的。这可能很危险。
【解决方案3】:

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

如果您没有遇到任何问题,就性能而言,您仍然可以坚持使用列表和标准函数。然后当您遇到性能问题时进行切换。

正如@u0b34a0f6ae 在 cmets 中所提到的,在开始时使用生成器可以让您更轻松地扩展到更大的数据集。

【讨论】:

  • +1 生成器使您的代码更适合大型数据集,而无需您进行预期。
【解决方案4】:

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

分析您的代码是了解您所做的事情是否有任何影响的唯一方法。

xrange、生成器等的大多数用法都是超过静态大小的小型数据集。只有当您处理大型数据集时,它才会真正发挥作用。 range() 与 xrange() 主要只是让代码看起来更丑一点,并且不会丢失任何东西,并且可能会有所收获。

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

【讨论】:

  • 个人资料,确实。其中一天,我将尝试进行经验比较。在那之前,我只是希望别人已经有了。 :)
  • 个人资料,个人资料,个人资料。我完全同意。个人资料,个人资料,个人资料。
【解决方案5】:

就性能而言,我想不出任何时候您会想要使用列表而不是生成器。

【讨论】:

  • all(True for _ in range(10 ** 8)) 在 Python 3.8 中比 all([True for _ in range(10 ** 8)]) 慢。我更喜欢这里的列表而不是生成器
【解决方案6】:

我从未发现生成器会阻碍您尝试执行的操作。然而,在很多情况下,使用生成器对你没有任何帮助。

例如:

sorted(xrange(5))

不提供任何改进:

sorted(range(5))

【讨论】:

  • 这些都没有比range(5) 提供任何改进,因为结果列表已经排序。
【解决方案7】:

关于性能:如果使用 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

【讨论】:

  • 那是因为 psyco 根本不加速生成器,所以它更多的是 psyco 的缺点而不是生成器。不过答案很好。
  • 另外,psyco 现在几乎无人维护。所有开发人员都花时间在 PyPy 的 JIT 上,据我所知,它优化了生成器。
【解决方案8】:

如果您需要为以后的其他内容保留这些值并且集合的大小不太大,您应该更喜欢列表推导式。

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

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

【讨论】:

  • 如果您只需要对流进行有限的前瞻/后视,那么itertools.tee() 可能会为您提供帮助。但一般来说,如果您想要多次传递,或随机访问一些中间数据,请对其进行列表/集合/字典。
【解决方案9】:

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

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 复制连接来避免两次传递?
  • 谢谢!我不知道字符串连接行为。您能否提供或链接到解释为什么需要两次通过?
  • @DavidEyk str.join 将所有字符串片段的长度加起来,这样它就知道要为组合的最终结果分配多少内存。第二遍将字符串片段复制到新缓冲区中以创建单个新字符串。见hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
  • 有趣的是,我经常使用生成器来加入 srings。但是,我想知道,如果它需要两次通过,它是如何工作的?例如''.join('%s' % i for i in xrange(10))
  • @ikaros45 如果join 的输入不是一个列表,它必须做额外的工作来为两个通道构建一个临时列表。大致这个``data = data if isinstance(data, list) else list(data); n = 总和(地图(长度,数据));缓冲区 = 字节数组(n); ... ```.
【解决方案10】:

生成器构建和可枚举的值列表。当迭代过程可以按需使用值时,可枚举是有用的。构建生成器需要时间,所以如果列表是数百万条记录,使用 sql server 处理 sql 中的数据可能更有用。

【讨论】:

    猜你喜欢
    • 2011-03-10
    • 2011-09-13
    • 1970-01-01
    • 2019-02-28
    • 2019-08-25
    • 1970-01-01
    • 2010-10-08
    • 1970-01-01
    相关资源
    最近更新 更多