【问题标题】:python generator is too slow to use it. why should I use it? and when?python生成器使用起来太慢了。我为什么要使用它?什么时候?
【发布时间】:2018-08-22 21:00:36
【问题描述】:

最近有人问我iteratorlist comprehensioniter(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 

generator 比其他东西慢。 不知道什么时候有用?

【问题讨论】:

  • 这不是衡量计算时间的非常准确的方法...我建议您尝试使用timeit
  • @przemo_li 取决于 python 版本; xrange 在 python3 中不存在。
  • 另外值得指出的是,当您进行计时时,list_comp_a 不是列表推导,它是使用列表推导创建的列表
  • 适当的测试不会显示生成器的相同极端分歧:repl.it/repls/IndianredImmediateTests
  • @przemo_li 看起来您不了解 Python 中的“迭代器”、“可迭代”和“生成器”是什么,也不了解它们与惰性求值的关系。 Py2 的range() 是一个返回list 的函数(它确实是可迭代的,但不是迭代器),xrange() 是一个实现“可迭代”协议以在迭代期间延迟生成值但也不是生成器的类.生成器是generator 类(通用可迭代类)的实例,它由生成器表达式或生成器函数(使用“yield”关键字的函数)构建。

标签: python performance generator list-comprehension


【解决方案1】:

generators 不要一次性将所有元素存储在内存中。他们yield 一次一个,这种行为使他们的内存效率更高。因此,您可以在内存受限时使用它们。

【讨论】:

  • 这个答案突出了 generator 相对于其他构造的独特优势。它应该是市场公认的答案。
  • 我还建议添加生成器还允许您从高延迟源构建数据。例如,如果您正在查询数据库,那么使用生成器您可以在收到结果后立即启动 yielding 结果,而无需等待完整的数据集
  • @SimonFraser 逻辑正确。
  • 只是觉得对提问者来说可能并不明显:)
  • @SimonFraser 这可能是他的when ?的答案
【解决方案2】:

作为序言:您的整个基准测试完全是错误的 - “list_comp_a”测试不使用列表理解测试列表的构建时间(“iter_list_comp_a”fwiw 也不测试),并且使用iter() 的测试大部分是无关紧要的 - iter(iterable) 只是 iterable.__iter__() 的快捷方式,只有在你想操纵迭代器本身时才有用,这实际上很少见。

如果你希望得到一些有意义的结果,你想要的基准是列表解析、生成器表达式和生成器函数的执行。为了测试它们的执行,最简单的方法是将所有三个案例包装在函数中,一个执行一个列表推导,另外两个从 resp 构建列表。生成器表达式和从生成器函数构建的生成器)。在所有情况下,我都使用xrange 作为真正的来源,所以我们只对有效差异进行基准测试。此外,我们使用timeit.timeit 进行基准测试,因为它比手动弄乱time.time() 更可靠,并且实际上是对小代码sn-ps 进行基准测试的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 类型 - 在某种程度上避免将所有内容加载到内存中并且只动态生成值.这里生成器表达式和函数(以及底层的generator 类)是实现惰性迭代的通用方法,无需编写自己的可迭代/迭代器(就像内置的property 类是使用自定义descriptors 而无需编写的通用方法你自己的描述符类)。

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

从可迭代对象中返回元素,直到用完为止。 然后无限期地重复这个序列。

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

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

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

【讨论】:

  • 布鲁诺,你上面提到的博客说In Python, "functions" with these capabilities are called generators,在你对我的回答的评论中说I am confusing "generators" with "generators functions". 我用谷歌搜索了generatorsgenerators functions,并没有发现任何与他们有关的东西.你有什么可以解释generatorsgenerator functions 的不同之处吗,比如treyhunner.com/2018/02/python-range-is-not-an-iterator 博客解释了为什么range 不是iterator
  • 很简单:generator 是一个内置类。调用生成器函数返回 generator 实例,评估生成器表达式也返回 generator 实例。因此,虽然我们经常使用术语“生成器”来表示生成器函数和生成器表达式,因为它们都是 create 生成器,但它们本身实际上并不是“生成器”。就像我们通常将“方法”称为“类”语句中定义的函数,而实际上它实际上是一个函数,并且只有在查找对象时才成为正确的method实例。
【解决方案3】:

我想我问了一个错误的问题,也许吧。 在原始代码中,这是不正确的,因为 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 已经提到的那样,该列表的性能更好,但是当内存大小不够时,list 的总和太高n 会使计算机变慢,但iterator 的总和太高高n,也许它真的需要很多时间来计算,但并没有让电脑变慢。

谢谢大家。 当我必须计算大量数据时,生成器会更好。 但是,

【讨论】:

  • 您的基准仍然是错误的,因为它将苹果与橙子进行了比较。例如,list_comp_a 只是一个普通列表,因此测试不会考虑实际构建该列表所需的时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-19
  • 2014-08-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多