好吧,所以我可能不应该担心这个问题,但是我有一些代码可以通过一组过滤器和地图传递一个(可能很长,可能很短)的可能性列表。其他的事情,我想知道我的实施是否会表现良好。
作为我想要做的事情的一个例子,考虑这个操作链:
现在,在完成所有这些废话之后,我想通过这组对[i,j]查看满足某个条件的一对。通常,解决方案是第一个条目之一,在这种情况下,我甚至不查看任何其他条目。但是,有时我必须使用整个列表,而且我找不到答案而且必须抛出错误。
我想将我的“操作链”实现为一系列生成器,即每个操作迭代前一个生成器生成的项,并逐项“生成”其自己的输出(一个SICP流)。这样,如果我从不查看输出的最后300个条目,它们甚至不会被处理。我知道itertools提供了像imap和ifilter之类的东西来完成我想要执行的许多类型的操作。
我的问题是:在我必须迭代所有可能性的情况下,一系列嵌套生成器是否会成为主要的性能影响?
答案 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
中看到的那样,这是最慢的,它很难附加单个列表,从而导致性能下降。 foo3
和foo4
几乎相同,因此创建临时列表似乎不是瓶颈,因为它只在整个迭代中创建一次。如果没有生成器,您很快就会遇到一些性能问题,如附加列表或创建临时列表。所以懒惰的评估就是为了给这些性能瓶颈带来优势。
答案 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
模块中的ifilter
,izip
和itertools
,我发现它们表现良好。虽然每次调用它们都会创建一个新的迭代器对象,但它相当轻量级,有点像一个从不包含多个项目的列表。此外,在CPython中,这些是用C实现的,因此非常有效。
在封面下,用纯Python实现的迭代器有一个next
方法,可以调用它来检索每个数据。方法调用的成本并不高,但也不是零。因此,如果您的代码将被用于必须尽可能优化的紧密循环中,我的建议如下:
imap
,ifilter
和izip
,而不是map
,filter
和zip
来构建结果列表在记忆中并返回它们。如果您有使用基于列表的版本的代码,那么通过更改为基于迭代器的版本,您将看到很大的改进。itertools
模块包含其他函数,例如takewhile
,starmap
,chain
和chain.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中map
,filter
和zip
返回迭代器。)
答案 2 :(得分:1)
“嵌套”迭代器相当于迭代器实现的函数的组合,因此通常它们没有特别新颖的性能考虑因素。
请注意,因为生成器是惰性的,与重复分配一个序列以转换为另一个序列相比,它们也倾向于减少内存分配。