首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >与列表相比,在生成器上迭代多次的速度

与列表相比,在生成器上迭代多次的速度
EN

Stack Overflow用户
提问于 2016-11-25 14:34:42
回答 3查看 2.1K关注 0票数 4

我预计在多个循环的情况下,列表迭代要比使用生成器快得多,我的代码表明这是错误的。

我的理解是(操作是指定义元素的任何表达式):

  • 列表需要初始化n个操作。
  • 但是,列表上的每一个循环都只是从内存中获取一个元素。
  • 因此,列表上的m循环只需要n个操作。
  • 生成器不需要初始化任何操作。
  • 然而,循环遍历生成器运行的是动态操作。
  • 因此,生成器上的一个循环需要n个操作。
  • 但是生成器上的m循环需要n×m运算。

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

代码语言:javascript
运行
复制
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)

结果让我吃惊..。

代码语言:javascript
运行
复制
Loops over list took:  0.20484769299946493
Loops over generator took:  0.0019217690005461918

不知何故,即使在循环1000次以上时,使用生成器的速度也比列表快得多。在这种情况下,我们讨论的是两个数量级!为什么?

编辑:

谢谢你的回答。现在我看到了我的错误。我错误地认为生成器从一个新的循环开始,就像range:

代码语言:javascript
运行
复制
>>> x = range(10)
>>> sum(x)
45
>>> sum(x)
45

但这太天真了(range不是发电机.)

关于可能重复的注释:我的问题涉及生成器上的多个循环,这在另一个线程中没有解释。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2016-11-25 14:57:19

你的发电机实际上只循环一次。一旦用pow2_gen创建,g就会存储一个生成器;这是第一次通过loop使用这个生成器,并释放出StopIteration。其他时候,通过loopnext(g) (或者Python2中的g.next() )只是继续抛出StopIteration,因此,实际上g代表了一个空序列。

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

处理这个问题的另一个困难是调用append来构建列表,这可能是构建列表的最慢的方式。更多的情况是,列表是用列表理解来构建的。

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

代码语言:javascript
运行
复制
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))类似。

代码语言:javascript
运行
复制
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)快一个数量级。同样,这是因为创建的生成器仅被迭代一次,而不是列表上的多个循环。

正如您所假设的,在这个特定的场景中,构建一次列表并多次迭代它(time_loop(create_list))要比多次迭代生成器(time_apply(create_gen))要快。

列表和生成器之间的权衡将很大程度上取决于您正在创建的迭代器的大小。有了1000条商品,我希望列表会很快。有了100,000件物品,事情可能就不同了。

代码语言:javascript
运行
复制
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))

我得到的是:

代码语言:javascript
运行
复制
create big list: 209.748
create big gen: 0.023

Python占用700至800 MB内存,用于构建大列表;生成器几乎不使用任何东西。在Python中,内存分配和垃圾清理在计算上是很昂贵的,可以预见会使代码变慢;生成器是一种非常简单的方法,可以避免占用机器的RAM,并且会对运行时产生很大的影响。

票数 6
EN

Stack Overflow用户

发布于 2016-11-25 15:06:13

你的考试有问题。也就是说,生成器是不可重用的。一旦筋疲力尽,就不能再使用它,必须生成一个新的。例如:

代码语言:javascript
运行
复制
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的语句中重新创建生成器。

代码语言:javascript
运行
复制
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)

给予:

代码语言:javascript
运行
复制
Loops over list took:  0.24689035064701784
Loops over generator took:  13.551637053904571

现在,发生器比列表慢两个数量级。这是预期的,因为与序列上的迭代次数相比,序列的大小很小。如果n很大,那么列表的创建就会变慢。这是因为在追加新项时如何展开列表,以及创建时未传递给列表的最终大小。与生成器相比,增加迭代次数将加快列表的速度,因为生成器所需的工作量增加,而对于列表,则保持不变。由于n只有1000 (小),而repeatsn的主导地位,那么生成器就会慢一些。

票数 4
EN

Stack Overflow用户

发布于 2016-11-25 15:00:41

您的测试无法工作,因为您的生成器在第一次通过loop()时就耗尽了。这是列表优于生成器的优点之一,您可以多次迭代它们(代价是将完整的列表存储在内存中)。

这是一个例子。我使用的是生成器表达式和列表理解(这比在for循环中使用for更优化),但概念是相同的:

代码语言:javascript
运行
复制
>>> 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 
>>> 

对于等效的测试,应该在外部循环的每一次迭代之后重新构建生成器:

代码语言:javascript
运行
复制
>>> 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
>>>
票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/40807277

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档