嵌套的python生成器有多快?

时间:2012-04-13 15:20:00

标签: python generator

好吧,所以我可能不应该担心这个问题,但是我有一些代码可以通过一组过滤器和地图传递一个(可能很长,可能很短)的可能性列表。其他的事情,我想知道我的实施是否会表现良好。

作为我想要做的事情的一个例子,考虑这个操作链:

  • 获取1到100之间的所有数字
  • 只保留偶数
  • 平方每个数字
  • 在上面的列表中生成所有对[i,j],在[1,2,3,4,5]中生成j
  • 仅保留i + j>对的对40

现在,在完成所有这些废话之后,我想通过这组对[i,j]查看满足某个条件的一对。通常,解决方案是第一个条目之一,在这种情况下,我甚至不查看任何其他条目。但是,有时我必须使用整个列表,而且我找不到答案而且必须抛出错误。

我想将我的“操作链”实现为一系列生成器,即每个操作迭代前一个生成器生成的项,并逐项“生成”其自己的输出(一个SICP流)。这样,如果我从不查看输出的最后300个条目,它们甚至不会被处理。我知道itertools提供了像imap和ifilter之类的东西来完成我想要执行的许多类型的操作。

我的问题是:在我必须迭代所有可能性的情况下,一系列嵌套生成器是否会成为主要的性能影响?

3 个答案:

答案 0 :(得分:3)

我尝试了两个实现,一个使用生成器,另一个没有生成器。我在2.7中测试了它,因此range返回一个列表而不是一个迭代器。

这是实现

使用生成器

def foo1():
    data = ((a,b) for a in (i*i for i in xrange(1,101) if i%2) for b in [1,2,3,4,5] if a+b > 40)
    return list(data)

没有发电机

def foo2():
    result=[]
    for i in range(1,101):
        if i%2:
            i=i*i
            for j in [1,2,3,4,5]:
                if i+j > 40:
                    result+=[(i,j)]
    return result

混合两者以便不附加列表

def foo3():
    data=[(a,b) for a in (i*i for i in range(1,101)) for b in [1,2,3,4,5] if a+b > 40] 
    return data

创建临时列表

def foo4():
    data=[(a,b) for a in [i*i for i in range(1,101)] for b in [1,2,3,4,5] if a+b > 40]
    return data

以下是我的结果

>>> t1=timeit.Timer("foo1()","from __main__ import foo1")
>>> t2=timeit.Timer("foo2()","from __main__ import foo2")
>>> t3=timeit.Timer("foo3()","from __main__ import foo3")
>>> t4=timeit.Timer("foo4()","from __main__ import foo4")

>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=10000)/10000)
100.95 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=10000)/10000)
158.90 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t3.timeit(number=10000)/10000)
130.02 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t4.timeit(number=10000)/10000)
133.68 usec/pass
>>> 

结论:

生成器表达式功能强大,您可以将其优化到更大的范围。正如您在示例foo2中看到的那样,这是最慢的,它很难附加单个列表,从而导致性能下降。 foo3foo4几乎相同,因此创建临时列表似乎不是瓶颈,因为它只在整个迭代中创建一次。如果没有生成器,您很快就会遇到一些性能问题,如附加列表或创建临时列表。所以懒惰的评估就是为了给这些性能瓶颈带来优势。

答案 1 :(得分:2)

根据官方文档,使用生成器表达式基本上等同于调用imap,因为它创建了一个迭代器。 (“A generator expression yields a new generator object.”)没有明确讨论嵌套表达式是创建单独的(组合的)对象还是内部具有复杂逻辑的单个表达式,但想象自己是解释器实现者,嵌套对象似乎是最直接的方式实现嵌套的生成器表达式。

然而,在决定哪些方面表现更好时,还有其他因素在起作用。我已经了解到,尽量减少短期对象的创建是性能的一个重要因素,而在Python中,有时很难注意到这一点。

表现不佳:(f(x) for x in range(100)) # builds 100-element list

更好的表现:(f(x) for x in xrange(100)) # uses counting iterator

我在我自己的实现中始终使用imap模块中的ifilterizipitertools,我发现它们表现良好。虽然每次调用它们都会创建一个新的迭代器对象,但它相当轻量级,有点像一个从不包含多个项目的列表。此外,在CPython中,这些是用C实现的,因此非常有效。

在封面下,用纯Python实现的迭代器有一个next方法,可以调用它来检索每个数据。方法调用的成本并不高,但也不是零。因此,如果您的代码将被用于必须尽可能优化的紧密循环中,我的建议如下:

  • 绝对尽可能使用imapifilterizip,而不是mapfilterzip来构建结果列表在记忆中并返回它们。如果您有使用基于列表的版本的代码,那么通过更改为基于迭代器的版本,您将看到很大的改进。
  • itertools模块包含其他函数,例如takewhilestarmapchainchain.from_iterable,这些函数在链式迭代器实现中通常很有用。
  • 不是将ifilter的多个应用程序链接起来,而是在可能的情况下组合传递的函数。例如,代替ifilter(lambda v: v > 0, ifilter(lambda v: v % 3 == 0, data)),将过滤器组合为ifilter(lambda v: (v > 0) and (v % 3 == 0), data)。在某些情况下,重新排列操作顺序可能是有效的,这样你就可以用这种方式折叠它们。
  • 当您执行地图操作以实现副作用并且对结果不感兴趣时​​,您可以使用此代替map来避免将结果累积到内存中:< / p>

    def consume(i):
      u'eat all contents of given iterator'
      while True:
        i.next()
    
    consume(imap(side_effect, data))
    

最后,要注意可能会增加内存使用量的其他陷阱,或者不必要地反复创建和销毁对象,强调垃圾收集器。这与迭代器没有任何关系,但确实会影响性能。下面的函数在内存中创建一个lambda表达式,并在每次调用时抛出它:

def foo(data):
  return reduce(R, imap(bar, ifilter(lambda v: v % 5 == 0, data)))

修复它的一种方法(这个方法每次仍然会创建两个迭代器对象,这是必要的,但不是额外的lambda表达式):

_mod5zero = lambda v: v % 5 == 0
def foo(data):
  return reduce(R, imap(bar, ifilter(_mod5zero, data)))

(注意:答案适用于Python 2.在Python 3中mapfilterzip返回迭代器。)

答案 2 :(得分:1)

“嵌套”迭代器相当于迭代器实现的函数的组合,因此通常它们没有特别新颖的性能考虑因素。

请注意,因为生成器是惰性的,与重复分配一个序列以转换为另一个序列相比,它们也倾向于减少内存分配。