【问题标题】:What does sum do?总和是做什么的?
【发布时间】:2018-06-01 07:05:57
【问题描述】:

首先,我想测试生成器和列表理解之间的内存使用情况。这本书给了我一个小台代码sn-p,我在我的PC(python3.6,Windows)上运行它,发现了一些意想不到的东西。

  1. 书上说,因为列表理解必须创建一个真正的列表并为其分配内存,所以从列表理解中迭代必须比从生成器中迭代慢。
  2. 当然,列表理解比生成器使用更多内存。

以下是我的代码,不满足先前的意见(在 sum 函数内)。

import tracemalloc
from time import time


def timeIt(func):
    start = time()
    func()
    print('%s use time' % func.__name__, time() - start)
    return func


tracemalloc.start()

numbers = range(1, 1000000)


@timeIt
def lStyle():
    return sum([i for i in numbers if i % 3 == 0])


@timeIt
def gStyle():
    return sum((i for i in numbers if i % 3 == 0))


lStyle()

gStyle()

shouldSize = [i for i in numbers if i % 3 == 0]

snapshotL = tracemalloc.take_snapshot()
top_stats = snapshotL.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
    print(stat)

输出:

lStyle use time 0.4460000991821289
gStyle use time 0.6190001964569092
[ Top 10 ]
F:/py3proj/play.py:31: size=11.5 MiB, count=333250, average=36 B
F:/py3proj/play.py:33: size=448 B, count=1, average=448 B
F:/py3proj/play.py:22: size=136 B, count=1, average=136 B
F:/py3proj/play.py:17: size=136 B, count=1, average=136 B
F:/py3proj/play.py:14: size=76 B, count=2, average=38 B
F:/py3proj/play.py:8: size=34 B, count=1, average=34 B

两点:

  • 生成器使用更多的时间和相同的内存空间。
  • sum 函数中的列表理解似乎无法创建总列表

我想也许 sum 函数做了一些我不知道的事情。谁能解释一下?

这本书是 High Perfoamance Python.chapter 5.But I do sth own different from the book to check the evidence in other context.他的代码在这里book_code,他没有把列表理解放在 sum 函数中。

【问题讨论】:

  • 你正在针对一个列表测试一个元组,没有生成器
  • @EvgenyPogrebnyak 没有元组理解之类的东西,括号中的生成器表达式是生成器表达式。参见例如stackoverflow.com/q/16940293/3001761.
  • 我错了! type((x for x in [1,2,3])) 确实是一个生成器
  • 我认为你应该说是哪本书告诉你的。
  • 好的,我补充一下@BoarGules

标签: python sum generator python-internals


【解决方案1】:

在时间性能测试方面,我确实依赖timeit 模块,因为它会自动执行代码的多次运行。

在我的系统上,timeit 给出以下结果(由于运行次数众多,我强烈减小了大小):

>>> timeit.timeit("sum([i for i in numbers if i % 3 == 0])", "numbers = range(1, 1000)")
59.54427594248068
>>> timeit.timeit("sum((i for i in numbers if i % 3 == 0))", "numbers = range(1, 1000)")
64.36398425334801

因此生成器慢了大约 8% (*)。这并不奇怪,因为生成器必须即时执行一些代码才能获得下一个值,而预先计算的列表只会增加其当前指针。

恕我直言,内存评估更复杂,所以我使用了来自 activestate 的Compute Memory footprint of an object and its contents (Python recipe)

>>> numbers = range(1, 100)
>>> numbers = range(1, 100000)
>>> l = [i for i in numbers if i % 3 == 0]
>>> g = (i for i in numbers if i % 3 == 0)
>>> total_size(l)
1218708
>>> total_size(g)
88
>>> total_size(numbers)
48

我的解释是,列表的所有项目都使用内存(这并不奇怪),而生成器只需要很少的值和一些代码,因此生成器的内存占用要少得多。

我强烈认为您已将tracemalloc 用于某些不适合的用途。它旨在搜索可能的内存泄漏(大块内存从未被释放),而不是控制单个对象使用的内存。


注意:我只能测试小尺寸。但是对于非常大的大小,列表可能会耗尽可用内存,并且机器将使用交换中的虚拟内存。在这种情况下,列表版本会变得更慢。更多详情there

【讨论】:

  • 首先,谢谢您对时间问题的解释,我接受了。此外,我不反对列表理解使用比生成器更多的内存。但是,最令人困惑的是列表- sum 函数中的 comp 看起来像生成器(在内存方面),它不同于“创建一个 list-comp 然后将其放入 sum 函数”。
  • This is not really a surprize because the generator has to execute some code on the fly... 我怀疑这只是故事/解释的一半。尝试一个简单的for 循环:c = 0; for i in range(n): if i % 3 == 0: c += i 这也必须做一些事情才能获得下一个值,但更快。我的直觉是,对于生成器,__next__ 的实现基本上很慢。
  • @jpp 确定幕后真正发生的事情需要深入研究 Python 解释器源代码。但是生成器添加了一些可能需要付出代价的功能......
  • @jpp 这并不奇怪。迭代生成器需要大量开销,但是一旦你开始使用 range(1000000) 大小的东西,生成器表达式就会胜过列表理解。
  • @juanpa.arrivillaga:我已经用你的评论更新了我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-06-09
  • 2015-05-22
  • 2011-04-28
  • 1970-01-01
  • 2013-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多