2016-11-25 51 views
4

我预计,在多重循环列表进行迭代的情况下,会比使用生成快得多,和我的代码表明这是假的了发电机迭代多次。速度相比于列表

我的理解是(通过操作我的意思是定义一个元素任意表达式):

  • 列表需要ň操作进行初始化
  • 但随后在列表中的每个循环就可以抓取从存储器
  • 从而一个元件,遍历列表仅需要ñ操作
  • 发电机不需要任何操作来进行初始化
  • 然而,上循环发电机运行在飞行操作
  • 因此,一个循环在发电机需要ň操作
  • 遍历发电机需要的n×m操作

我用下面的代码检查了我的期望:

from timeit import timeit 

def pow2_list(n): 
    """Return a list with powers of 2""" 

    results = [] 

    for i in range(n): 
     results.append(2**i) 

    return results 

def pow2_gen(n): 
    """Generator of powers of 2""" 

    for i in range(n): 
     yield 2**i 

def loop(iterator, n=1000): 
    """Loop n times over iterable object""" 

    for _ in range(n): 
     for _ in iterator: 
      pass 

l = pow2_list(1000) # point to a list 
g = pow2_gen(1000) # point to a generator 


time_list = \ 
    timeit("loop(l)", setup="from __main__ import loop, l", number=10) 

time_gen = \ 
    timeit("loop(g)", setup="from __main__ import loop, g", number=10) 

print("Loops over list took: ", time_list) 
print("Loops over generator took: ", time_gen) 

而且结果让我吃惊......

Loops over list took: 0.20484769299946493 
Loops over generator took: 0.0019217690005461918 

不知怎的,使用发电机循环1000次以上,即使出现比列表更快。在这种情况下,我们谈论的是两个数量级!为什么?

编辑:

感谢您的答案。现在,我看到我的错误。我错误地认为发电机从一个新的循环开始,如范围:

>>> x = range(10) 
>>> sum(x) 
45 
>>> sum(x) 
45 

但这是天真的(范围不是发电机...)。

关于可能的重复评论:我的问题涉及到生成器的多个循环,这在其他线程中没有解释。

+1

你假设生成器更快是不正确的。可能的重复[生成器与Python中的列表理解性能](http://stackoverflow.com/questions/30112326/generators-vs-list-comprehension-performance-in-python) – AChampion

+2

速度差异是两个数量级的差异应该提醒你注意事项,即你的测试有问题。试试'loop(pow_2_gen(1000))'以获得准确的结果。 – Dunes

+0

您的测试是否有瑕疵。一个函数必须在内存中创建一个完整的列表,另一个函数只能返回一个迭代器。建议使用@Dunes来获得更准确的结果。 –

回答

5

您的发电机实际上只循环一次。一旦与pow2_geng存储发生器产生;是首次通过loop,该发电机被消耗,并发出StopIteration。其他时间通过loop,next(g)(或Python 2中的g.next())只是继续抛出StopIteration,因此,实际上g表示一个空序列。

为了使比较更公平,每次循环时都需要重新创建生成器。

您接触到这个方法的另一个困难是您打电话给append来建立您的列表,这可能是构建列表最慢的方式。更多的时候,列表是通过列表解析构建的。

下面的代码可以让我们更仔细地分辨时序。 create_listcreate_gen分别使用列表理解和生成器表达式创建列表和生成器。 time_loop就像你的loop方法,而time_applyloop的一个版本,每次在循环中重新创建迭代。

def create_list(n=1000): 
    return [2**i for i in range(n)] 

def create_gen(n=1000): 
    return (2**i for i in range(n)) 

def time_loop(iterator, n=1000): 
    for t in range(n): 
     for v in iterator: 
      pass 

def time_apply(create_fn, fn_arg, n=1000): 
    for t in range(n): 
     iterator = create_fn(fn_arg) 
     time_loop(iterator, 1) 

print('time_loop(create_list): %.3f' % timeit("time_loop(create_list(1000))", 
               setup="from __main__ import *", 
               number=10)) 

print('time_loop(create_gen): %.3f' % timeit("time_loop(create_gen(1000))", 
              setup="from __main__ import *", 
              number=10)) 

print('time_apply(create_list): %.3f' % timeit("time_apply(create_list, 1000)", 
               setup="from __main__ import *", 
               number=10)) 

print('time_apply(create_gen): %.3f' % timeit("time_apply(create_gen, 1000)", 
               setup="from __main__ import *", 
               number=10)) 

在我的箱子结果表明,建立一个列表(time_apply(create_list))是建立一个发电机(time_apply(create_gen))的时间相似(或者甚至快于)。

time_loop(create_list): 0.244 
time_loop(create_gen): 0.028 
time_apply(create_list): 21.190 
time_apply(create_gen): 21.555 

你可以看到你在你的问题,这是time_loop(create_gen)是一个数量级比time_loop(create_list)速度已经记录了同样的效果。同样,这是因为创建的生成器只是迭代一次,而不是列表中的许多循环。

正如你hypothesise,一旦建立一个列表,并遍历了很多次(time_loop(create_list))比在此特定情形迭代一个发电机多次(time_apply(create_gen))更快。

列表和生成器之间的权衡将强烈依赖于您创建的迭代器的大小。有1000个项目,我希望列表速度非常快。有了100,000件商品,情况可能会有所不同。

print('create big list: %.3f' % timeit("l = create_list(100000)", 
             setup="from __main__ import *", 
             number=10)) 

print('create big gen: %.3f' % timeit("g = create_gen(100000)", 
             setup="from __main__ import *", 
             number=10)) 

在这里我得到:

create big list: 209.748 
create big gen: 0.023 

Python使用了700和800 MB内存建设大名单之间;发电机几乎没有任何使用。在Python中,内存分配和垃圾清理在计算上花费很大,并且可预见地让你的代码变慢;发生器是避免吞噬机器内存的一种非常简单的方法,并且可以对运行时产生很大的影响。

+0

也介意给我们结果呢? –

2

您的测试不起作用,因为您的发电机在loop()的第一阶段耗尽。这是列表相对于生成器的优点之一,您可以多次迭代它们(以将完整列表存储在内存中为代价)。

下面是对此的一个说明。我使用的是一台发电机的表达和列表理解(这是不是在for循环使用append更加优化),但概念是相同的:

>>> gen = (i for i in range(3)) 
>>> for n in range(2): 
...  for i in gen: 
...   print(i) 
... 
0 # 1st print 
1 
2 # after one loop the iterator is exhausted 
>>> 
>>> lst = [x for x in range(3)] 
>>> for n in range(2): 
...  for i in lst: 
...   print(i) 
... 
0 # 1st print 
1 
2 
0 # 2nd print 
1 
2 
>>> 

对于等效的测试,你应该每次迭代后重建发电机外环:

>>> for n in range(2): 
...  gen = (i for i in range(3)) 
...  for i in gen: 
...   print(i) 
... 
0 # 1st print 
1 
2 
0 # 2nd print 
1 
2 
>>> 
4

您的测试存在问题。即,发电机不可重复使用。一旦用尽,就不能再使用,必须生成新的。例如。

l = [0, 1, 2, 4, 5] 
g = iter(l) # creates an iterator (a type of generator) over the list 

sum_list0 = sum(l) 
sum_list1 = sum(1) 
assert sum_list0 == sum_list1 # all working normally 

sum_gen0 = sum(g) # consumes generator 
sum_gen1 = sum(g) # sum of empty generator is 0 
assert sum_gen0 == sum_list1 # result is correct 
assert sum_gen1 == sum_list1, "second result was incorrect" # because generator was exhausted 

为您的测试工作,你必须在你传递给timeit语句重新重新发电机。

from timeit import timeit 

n = 1000 
repeats = 10000 

list_powers = [2**i for i in range(n)] 
def gen_powers(): 
    for i in range(n): 
     yield 2**i 

time_list = timeit("min(list_powers)", globals=globals(), number=repeats) 
time_gen = timeit("min(gen_powers())", globals=globals(), number=repeats) 

print("Loops over list took: ", time_list) 
print("Loops over generator took: ", time_gen) 

给出:

Loops over list took: 0.24689035064701784 
Loops over generator took: 13.551637053904571 

现在发电机是数量级比列表慢两个数量级。这是预期的,因为序列的大小与序列上的迭代次数相比较小。如果n很大,则列表创建将变慢。这是因为附加新项目时列表如何展开,并且最终大小在创建时未传递给列表。随着发电机所需工作量的增加,增加迭代次数将使发电机列表与发电机相比更快,而列表中的数据保持不变。由于n只有1000(小),并且repeats支配n,所以发生器较慢。