代码之家  ›  专栏  ›  技术社区  ›  frhyme

python生成器速度太慢,无法使用它。我为什么要使用它?什么时候?

  •  2
  • frhyme  · 技术社区  · 6 年前

    最近我有一个问题,那就是哪一个是最快的 iterator ,则, list comprehension ,则, iter(list comprehension) generator 。 然后编写如下简单代码。

    n = 1000000
    iter_a = iter(range(n))
    list_comp_a = [i for i in range(n)]
    iter_list_comp_a = iter([i for i in range(n)])
    gene_a = (i for i in range(n))
    
    import time
    import numpy as np
    
    for xs in [iter_a, list_comp_a, iter_list_comp_a, gene_a]:
        start = time.time()
        np.sum(xs)
        end = time.time()
        print((end-start)*100)
    

    结果如下。

    0.04439353942871094 # iterator
    9.257078170776367 # list_comprehension
    0.006318092346191406 # iterator of list_comprehension
    7.491207122802734 # generator 
    

    发电机比其他东西慢。 我不知道什么时候有用?

    3 回复  |  直到 6 年前
        1
  •  22
  •   Kishor Pawar    6 年前

    generators 不要一次性将所有元素存储在内存中。他们 yield 一次一个,这种行为可以提高内存效率。因此,当内存是一个约束时,可以使用它们。

        2
  •  1
  •   bruno desthuilliers    6 年前

    作为前言:“list\u comp\u a”测试没有使用列表理解测试列表的构建时间(“iter\u list\u comp\u a”fwiw也没有),测试使用 iter() 都是无关紧要的- iter(iterable) 只是 iterable.__iter__() 并且只有在您想要操纵迭代器本身时才有用,这实际上是非常罕见的。

    如果您希望获得一些有意义的结果,那么您希望基准测试的是 处决 一个列表理解、一个生成器表达式和一个生成器函数。为了测试它们的执行情况,最简单的方法是将所有三种情况都封装在函数中,一种是执行列表理解,另两种是从resp生成列表。生成器表达式和从生成器函数生成的生成器)。在所有情况下,我使用 xrange 作为真正的来源,因此我们只对有效差异进行基准测试。我们还使用 timeit.timeit 执行基准测试,因为它比手动处理更可靠 time.time() ,实际上是pythonic标准的规范方法,用于对小代码段进行基准测试。

    import timeit
    # py2 / py3 compat
    try:
        xrange
    except NameError:
        xrange = range
    
    n = 1000
    
    def test_list_comp():
        return [x for x in xrange(n)]
    
    def test_genexp():
        return list(x for x in xrange(n))
    
    def mygen(n):
        for x in xrange(n):
            yield x
    
    def test_genfunc():
        return list(mygen(n))
    
    for fname in "test_list_comp", "test_genexp", "test_genfunc":
        result = timeit.timeit("fun()", "from __main__ import {} as fun".format(fname), number=10000)
        print("{} : {}".format(fname, result))
    

    在这里(使用了5年以上的标准桌面上的py 2.7.x),我得到了以下结果:

    test_list_comp : 0.254354953766
    test_genexp : 0.401108026505
    test_genfunc : 0.403750896454
    

    如您所见,列表理解速度更快,生成器表达式和生成器函数基本上与生成器表达式等效,但与生成器表达式相比有一个非常微小的优势(但如果重复测试,则保持不变)。

    现在回答你的主要问题 “为什么以及何时使用生成器”,答案有三个:1/内存使用,2/无限迭代和3/协同路由。

    第一点:内存使用。实际上,这里不需要生成器,只需要惰性迭代,它可以通过 writing your own iterable / iterable -比如说内置的 file 键入does-以避免在内存中加载所有内容,而只动态生成值。此处生成器表达式和函数(以及底层 generator 类)是一种实现惰性迭代的通用方法,无需编写自己的iterable/iterator(就像内置的 property 类是使用自定义 descriptors 无需编写自己的描述符类)。

    第二点:无限迭代。在这里,我们有一些从序列类型(列表、元组、集合、dicts、字符串等)中无法获得的东西,根据定义,这些序列类型是有限的。例如 the itertools.cycle iterator :

    返回iterable中的元素,直至其耗尽。 然后无限期重复该序列。

    请注意,这里的这种能力不是来自生成器函数或表达式,而是来自iterable/iterator协议。与内存使用优化相比,无限迭代的用例明显较少,但当您需要时,它仍然是一个方便的特性。

    最后是第三点:协同程序。嗯,这是一个相当复杂的概念,特别是你第一次读到它时,所以我让其他人来做介绍: https://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/

    这里有一些只有生成器才能提供的东西,而不是iterables/迭代器的便捷快捷方式。

        3
  •  0
  •   frhyme    6 年前

    我想我可能问错了问题。 在原始代码中,它是不正确的,因为 np.sum 效果不太好。 np.sum(iterator) 不会返回正确答案。所以,我修改了我的代码,如下所示。

    n = 10000
    iter_a = iter(range(n))
    list_comp_a = [i for i in range(n)]
    iter_list_comp_a = iter([i for i in range(n)])
    gene_a = (i for i in range(n))
    
    import time
    import numpy as np
    import timeit
    
    for xs in [iter_a, list_comp_a, iter_list_comp_a, gene_a]:
        start = time.time()
        sum(xs)
        end = time.time()
        print("type: {}, performance: {}".format(type(xs), (end-start)*100))
    

    然后,性能如下所示。性能 list 是最好的,迭代器不好。

    type: <class 'range_iterator'>, performance: 0.021791458129882812
    type: <class 'list'>, performance: 0.013279914855957031
    type: <class 'list_iterator'>, performance: 0.02429485321044922
    type: <class 'generator'>, performance: 0.13570785522460938
    

    就像前面提到的@Kishor Pawar一样,该列表的性能更好,但当内存大小不足时 列表 太高 n 使计算机变慢,但 iterator 太高 n ,也许它真的花了很多时间来计算,但并没有使计算机变慢。

    Thx适用于所有人。 当我必须计算大量数据时,生成器更好。 但是