【问题标题】:Generators vs List Comprehension performance in PythonPython 中的生成器与列表理解性能
【发布时间】:2015-07-18 16:34:30
【问题描述】:

目前我正在学习生成器和列表理解,并在使用探查器以查看性能提升时偶然发现了这个 cProfile,该 cProfile 是使用两者的大范围内的素数之和。

我可以看到在生成器中 :1 基因expr 的累积时间比它的列表对应物要短,但是第二行让我感到困惑。正在做一个电话,我认为检查数字是素数,但不应该是列表理解中的另一个 :1 模块?

我是否遗漏了个人资料中的某些内容?

In [8]: cProfile.run('sum((number for number in xrange(9999999) if number % 2 == 0))')
         5000004 function calls in 1.111 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  5000001    0.760    0.000    0.760    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    1.111    1.111 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.351    0.351    1.111    1.111 {sum}



In [9]: cProfile.run('sum([number for number in xrange(9999999) if number % 2 == 0])')
         3 function calls in 1.123 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    1.075    1.075    1.123    1.123 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.048    0.048    0.048    0.048 {sum}

【问题讨论】:

  • 我不认为他们是素数,只有偶数。
  • 你为什么期待另一个:1 module
  • 一个函数调用比另一个函数调用多 5000001 次,这难道不是令人惊讶的事情吗?这正是next 被调用的次数。
  • 在 Python 2 中你不会得到任何额外的列表解析行,但在 Python 3 中你会因为现在使它类似于生成器表达式一个额外的代码对象(&lt;listcomp&gt;)也是为列表理解创建的:stackoverflow.com/a/30097520/846892
  • 是的,对不起。我的意思是甚至。大脑冻结。 :D

标签: python profiling generator list-comprehension


【解决方案1】:

首先调用的是生成器对象的next(或Python 3 中的__next__)方法,而不是用于某些偶数检查。

在 Python 2 中,您不会为列表解析(LC)添加任何额外的行,因为 LC 不会创建任何对象,但在 Python 3 中,您将因为现在使其类似于生成器表达式而添加了一个代码对象(&lt;listcomp&gt;) 也是为 LC 创建的。

>>> cProfile.run('sum([number for number in range(9999999) if number % 2 == 0])')
         5 function calls in 1.751 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    1.601    1.601    1.601    1.601 <string>:1(<listcomp>)
        1    0.068    0.068    1.751    1.751 <string>:1(<module>)
        1    0.000    0.000    1.751    1.751 {built-in method exec}
        1    0.082    0.082    0.082    0.082 {built-in method sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

>>> cProfile.run('sum((number for number in range(9999999) if number % 2 == 0))')
         5000005 function calls in 2.388 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  5000001    1.873    0.000    1.873    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    2.388    2.388 <string>:1(<module>)
        1    0.000    0.000    2.388    2.388 {built-in method exec}
        1    0.515    0.515    2.388    2.388 {built-in method sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

虽然 1(LC) 与生成器表达式中的 5000001 相比,调用次数有所不同,这主要是因为 sum 正在消耗迭代器,因此必须调用其 __next__ 方法 500000 + 1 次(最后 1 可能是StopIteration 结束迭代)。对于列表理解,所有的魔法都发生在它的代码对象中,LIST_APPEND 帮助它将项目一个接一个地附加到列表中,即没有可见的对 cProfile 的调用。

【讨论】:

  • 然后你可以说使用生成器而不是列表理解的好处是,在运行结束时,你会在内存中以一个巨大的列表结束,该列表在最后收集,vs只能访问并立即删除号码的生成器?这就是这里的教训?
  • @cllamach 在特定大小之后是<genexpr> will beat list comprehension,除非我们确实需要该列表,否则无需在内存中创建一个。 str.join 是一个异常,如果我们向它提供一个列表而不是 ,它会表现良好。请注意,对于实际的基准测试,您应该使用 timeit 模块而不是 cProfile。
猜你喜欢
  • 2016-10-04
  • 1970-01-01
  • 2013-12-30
  • 2023-02-02
  • 2017-08-05
  • 2018-04-20
  • 2013-08-15
  • 2014-06-18
相关资源
最近更新 更多