Skip to content

Latest commit

 

History

History
62 lines (40 loc) · 3.83 KB

Counting-without-counting-in-Python.rst

File metadata and controls

62 lines (40 loc) · 3.83 KB

Python计数勿须计数

Python标准模式中执行n次循环体将导致n个整型对象的的内存分配与销毁,即使循环体并非真的需要这么多整型对象,一直以来,这让我很苦恼。

#产生100万个整数

for i in range(1000000):
    print

#每次产生一个整数

for i in xrange(1000000):
    print

是的,我知道,你会抱怨我也太容易苦恼了。range()模式是标准的,简单而又易读。其实现相比于我在循环中可能做的实际工作,确实要快得多。另外,在光明的未来,当我们都用上PyPy,这额外的百万个整型对象无论如何都会被优化掉的。

但是,无论在for循环中使用range()有多方便,"为循环创建百万个整数"仍旧多少显得有些笨拙---这个想法始终在我的脑中萦绕不去。因此,一方面,现实中我当然还是一直使用range()来编写for循环,另一方面我就在想是否存在具备更少 语义 开销的替代方案。

第一种替代方案,仍然创建一个长度为n的列表,但不需要创建一百万个真实的对象以供循环,而是利用列表乘法创建一个包含百万个指针项(item)的列表,所有的指针项都引用同一个项(如下所示例子中,就是无害的 None 值)

# 仅创建列表对象

for i in [None] * 1000000:
    print

这样,为了迭代,创建了一个额外的对象,概念上比较清晰,而且一些使用timeit进行的快速实验表明:这种方案显著地快于使用range(),即使还没有使用xrange()的那么快。但是,它还是必须分配正比于迭代次数的内存无用空间。

如果我们想要一种更优的解决方案,在其生命周期内,需要分配的内存少于O(n),那该怎么做呢?好吧,假如是在十年前写这篇博文,我可以花费一整天的时间构建一系列的越来越复杂的备选方案,每个都比上述方案需要更少的对象。为了让你有个直观的体会,举例来说,其中一种可能方案是利用同轴(concentric)循环

thousand_things = [None] * 1000
for i in thousand_things:
    for j in thousand_things:
        print

这样,内存占用就降至O(n0.5),因为我仅使用了两个包含一千个指针的列表。是的,我知道,这样还是会创建一千个额外的迭代器对象---每个都需要在一次内部循环中被保持着---但相比将一个百万个项的列表保存在内存中,这仍是一个巨大的进步。

想想看,利用一个列表,其元素是通过简单的列表操作产生的逐步递减的二进制数,我能将这个问题的空间复杂度降至O(log n),多么有趣啊!

但是一切并非如此---因为,当我最终在今晚坐下来准备享受这个过程,几分钟内我就发现itertools这个模块包含了一个解决方案,甚至不需要O(log n)仅需O(1)的内存使用。看好了,repeat迭代器

# 我们可以使用*None*一百万次
# 但不需要创建Python整型对象!

from itertools import repeat
for x in repeat(None, 1000000):
    print

对于C Python来说,这个方法是我们所讨论过的替代方案中最快的一种,不信的话,你就赶紧做些 timeit 实验核实一下吧。如果你是个 timeit 新手,可以使用如下所示的一个简单的命令行命令

$ python -m timeit \
>   -s 'from itertools import repeat' \
>   'for x in repeat(None, 1000000): pass'

100 loops, best of 3: 16.8 msec per loop

因此,虽然我追求的是一个并不实用的目标,却学会了一个有用的新的itertools诀窍,这一诀窍说不定哪天就会派上用场。我也可以经常将repeat()用于迭代,以防将嵌入式设备搞崩溃,毕竟嵌入式设备上每个字节都是很珍贵的。