Python 3:创建[func(i) for i in range(N)]列表推导的最有效方法

8

假设我有一个函数func(i),它为整数i创建一个对象,N是非负整数。那么创建一个与该列表相等的列表(不是范围),最快的方法是什么?

mylist = [func(i) for i in range(N)]

不需要使用高级方法,如在C中创建函数,是否有其他方法? 我对上述列表理解的主要担忧是,我不确定Python是否预先知道范围(N)的长度来预分配mylist,因此必须逐步重新分配列表。 是这种情况还是Python足够聪明,可以先将mylist分配到长度N,然后计算它的元素? 如果不行,创建mylist的最佳方法是什么? 或许这样做?

mylist = [None]*N
for i in range(N): mylist[i] = func(i)

重新编辑:已从以前的编辑中删除了误导性信息。

6个回答

7

有人写道:“Python很聪明。只要您正在迭代的对象具有__len____length_hint__方法,Python将调用它来确定大小并预分配数组。”

据我所知,在列表推导式中没有预分配。Python无法从输入的大小中得知输出的大小。

看看这个Python 2.6代码:

>>> def foo(func, iterable):
...     return [func(i) for i in iterable]
...
>>> import dis; dis.dis(foo)
  2           0 BUILD_LIST               0 #### build empty list
              3 DUP_TOP
              4 STORE_FAST               2 (_[1])
              7 LOAD_FAST                1 (iterable)
             10 GET_ITER
        >>   11 FOR_ITER                19 (to 33)
             14 STORE_FAST               3 (i)
             17 LOAD_FAST                2 (_[1])
             20 LOAD_FAST                0 (func)
             23 LOAD_FAST                3 (i)
             26 CALL_FUNCTION            1
             29 LIST_APPEND      #### stack[-2].append(stack[-1]); pop()
             30 JUMP_ABSOLUTE           11
        >>   33 DELETE_FAST              2 (_[1])
             36 RETURN_VALUE

它只是建立一个空列表,并附加迭代器提供的任何内容。

现在看看这段代码,其中列表推导式中有一个'if'语句:

>>> def bar(func, iterable):
...     return [func(i) for i in iterable if i]
...
>>> import dis; dis.dis(bar)
  2           0 BUILD_LIST               0
              3 DUP_TOP
              4 STORE_FAST               2 (_[1])
              7 LOAD_FAST                1 (iterable)
             10 GET_ITER
        >>   11 FOR_ITER                30 (to 44)
             14 STORE_FAST               3 (i)
             17 LOAD_FAST                3 (i)
             20 JUMP_IF_FALSE           17 (to 40)
             23 POP_TOP
             24 LOAD_FAST                2 (_[1])
             27 LOAD_FAST                0 (func)
             30 LOAD_FAST                3 (i)
             33 CALL_FUNCTION            1
             36 LIST_APPEND
             37 JUMP_ABSOLUTE           11
        >>   40 POP_TOP
             41 JUMP_ABSOLUTE           11
        >>   44 DELETE_FAST              2 (_[1])
             47 RETURN_VALUE
>>>

相同的代码,加上一些避免LIST_APPEND的代码。
在Python 3.X中,您需要深入挖掘:
>>> import dis
>>> def comprehension(f, iterable): return [f(i) for i in iterable]
...
>>> dis.dis(comprehension)
  1           0 LOAD_CLOSURE             0 (f)
              3 BUILD_TUPLE              1
              6 LOAD_CONST               1 (<code object <listcomp> at 0x00C4B8D
8, file "<stdin>", line 1>)
              9 MAKE_CLOSURE             0
             12 LOAD_FAST                1 (iterable)
             15 GET_ITER
             16 CALL_FUNCTION            1
             19 RETURN_VALUE
>>> dis.dis(comprehension.__code__.co_consts[1])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                18 (to 27)
              9 STORE_FAST               1 (i)
             12 LOAD_DEREF               0 (f)
             15 LOAD_FAST                1 (i)
             18 CALL_FUNCTION            1
             21 LIST_APPEND              2
             24 JUMP_ABSOLUTE            6
        >>   27 RETURN_VALUE
>>>

这是老生常谈的技巧:先建立一个空列表,然后迭代可迭代对象,在必要时将元素添加到列表中。我在这里看不到任何预分配。

你想到的优化方法是在单个操作码内使用,例如list.extend(iterable)的实现可以在iterable能够准确报告其长度时进行预分配。list.append(object)只接受单个对象而不是可迭代对象。


感谢您向我展示如何在Python中反汇编,这将在未来对我非常有帮助。但是看起来您正在使用Python 2,在Python 3中我得到不同的输出。它无法适应此评论,我很快会编辑我的问题以展示我的发现。 - mejiwa
啊,明白了!好的,你说服了我,我会把你的回答标记为被接受的答案。至于“我假设”、“可能”、“实际上”这些词:看来我需要提高一下我的英语水平 :-) 我会删除我添加的误导性信息。 - mejiwa
@Daniel:很高兴你还在这里,我以为我通过接受这个答案吓走了你。@John:只是为了100%确定:如果可迭代的对象能够报告其长度,你有没有证据表明list.extend(iterable)实际上会进行预分配?如果你没有,那也没问题。你坚持让我接受正确答案已经很好了 :) - mejiwa
@mejiwa:http://svn.python.org/view/python/branches/py3k/Objects/listobject.c?view=markup 在页面中按Ctrl-F搜索_PyObject_LengthHint - John Machin

3
如果您使用timeit模块,您可能会得出相反的结论:列表推导比预分配更快:
f=lambda x: x+1
N=1000000
def lc():
    return [f(i) for i in range(N)]
def prealloc():
    mylist = [None]*N
    for i in range(N): mylist[i]=f(i)
    return mylist
def xr():
    return map(f,xrange(N))

if __name__=='__main__':
    lc()

警告:这些是在我的计算机上得出的结果。你应该自己尝试这些测试,因为你的Python版本和硬件可能不同。(请参阅评论。)
% python -mtimeit -s"import test" "test.prealloc()"
10 loops, best of 3: 370 msec per loop
% python -mtimeit -s"import test" "test.lc()"
10 loops, best of 3: 319 msec per loop
% python -mtimeit -s"import test" "test.xr()"
10 loops, best of 3: 348 msec per loop

请注意,与Javier的答案不同,我在使用“预分配”方法时将mylist = [None]*N包括在timeit要计时的代码中。(它不仅是设置的一部分,因为只有在使用预分配时才需要这段代码。)
另外,time模块(以及time(UNIX)命令)可能会给出不可靠的结果。如果您想计时Python代码,我建议使用timeit模块。

这个map(f, xrange(N))的速度要快得多。 - Josh Lee
@jleedev:我添加了xr来测试map(f,xrange(N))。在CPython 2.6中,列表推导比map更快。 - unutbu
奇怪,我正在使用 cpython 2.6.1(在 Darwin 上),而“xr”比“lc”快约20%。 - Josh Lee
@jleedev:嗯,这很有趣。也许这是一个很好的提醒,最快的东西可能取决于很多因素,包括硬件。 - unutbu
刚在Pentium III的Linux系统上尝试了2.5.2版本,分别得到了3/2.63/2.82秒的结果。像许多问题一样,答案是“取决于情况”,虽然在Python中手动预分配数组通常没有什么用。 - Josh Lee
prealloc/lc/xr - 240/192/148 (python 2.6.4), 244/216/200 (3.1.1)。在两个版本中,map 稍微快一些。 - jfs

2
使用自动调整大小的数组和预分配数组在计算复杂度方面没有区别。最坏情况下,它的成本大约为O(2N)。详情请参见:Constant Amortized Time
函数调用的成本加上您的函数中发生的任何事情都将使这个额外的n变得微不足道。这不是您应该担心的事情。只需使用列表推导即可。

当然是个事实。O(2n) 等同于 O(n)。 - Josh Lee
我不知道它那么便宜,谢谢你的见解,我会看看的!但是分配的恒定开销仍然是我想要避免的东西。 - mejiwa

1

这里必须要不同意Javier...

以下是代码:

print '%5s | %6s | %6s' % ('N', 'l.comp', 'manual')
print '------+--------+-------'
for N in 10, 100, 1000, 10000:
    num_iter = 1000000 / N

    # Time for list comprehension.
    t1 = timeit.timeit('[func(i) for i in range(N)]', setup='N=%d;func=lambda x:x' % N, number=num_iter)

    # Time to build manually.
    t2 = timeit.timeit('''mylist = [None]*N
for i in range(N): mylist[i] = func(i)''', setup='N=%d;func=lambda x:x' % N, number=num_iter)

    print '%5d | %2.4f | %2.4f' % (N, t1, t2)

我得到了以下的表格:
    N | l.comp | manual
------+--------+-------
   10 | 0.3330 | 0.3597
  100 | 0.2371 | 0.3138
 1000 | 0.2223 | 0.2740
10000 | 0.2185 | 0.2771

从这个表格中可以看出,在这些不同长度的情况下,列表推导式比预分配更快。

1

有趣的问题。根据以下测试,似乎在当前的CPython实现中(Python 2代码但结果排名相同,除了Python 3中没有xrange),预分配并不能提高性能:

N = 5000000

def func(x):
    return x**2

def timeit(fn):
    import time
    begin = time.time()
    fn()
    end = time.time()
    print "%-18s: %.5f seconds" % (fn.__name__, end - begin)

def normal():
    mylist = [func(i) for i in range(N)]

def normalXrange():
    mylist = [func(i) for i in xrange(N)]

def pseudoPreallocated():
    mylist = [None] * N
    for i in range(N): mylist[i] = func(i)

def preallocated():
    mylist = [None for i in range(N)]
    for i in range(N): mylist[i] = func(i)

def listFromGenerator():
    mylist = list(func(i) for i in range(N))

def lazy():
    mylist = (func(i) for i in xrange(N))



timeit(normal)
timeit(normalXrange)
timeit(pseudoPreallocated)
timeit(preallocated)
timeit(listFromGenerator)
timeit(lazy)

结果(排名在括号中):

normal            : 7.57800 seconds (2)
normalXrange      : 7.28200 seconds (1)
pseudoPreallocated: 7.65600 seconds (3)
preallocated      : 8.07800 seconds (5)
listFromGenerator : 7.84400 seconds (4)
lazy              : 0.00000 seconds

但是使用 psyco.full()

normal            : 7.25000 seconds  (3)
normalXrange      : 7.26500 seconds  (4)
pseudoPreallocated: 6.76600 seconds  (1)
preallocated      : 6.96900 seconds  (2)
listFromGenerator : 10.50000 seconds (5)
lazy              : 0.00000 seconds

正如您所看到的,使用psyco进行伪预分配更快。无论如何,xrange解决方案(我建议使用)和其他解决方案之间没有太大区别。如果您以后不处理列表的所有元素,则还可以使用惰性方法(如上面的代码所示),它将创建一个生成器,按照您迭代它的时间产生元素。


我认为在Python 3中,range与xrange相同,并且xrange已被删除,所以我已经以某种方式使用xrange。而且对于我的需求,我需要访问列表中的所有元素,我甚至依赖于func(i)的执行。但是感谢所有这些基准测试,真的很有启发性。 - mejiwa
@mejiwa:是的,Python 3 的 range 现在与 Python 2 的 xrange 类似了。我在我的答案中添加了一条评论。如果你将基准测试与 Python 3.1 进行比较,你会注意到它只需要 2-3 秒,而不是 6-7 秒,这表明实现得到了多大的改进。也许你想尝试 Unladen Swallow(可能更快)。 - AndiDog
我的问题的动机更多是想知道在我的未来代码中选择什么策略,而不是优化单个问题,或多或少与Python实现无关。实际上,我甚至不能选择实现,因为它是用于blender导出脚本。但是对于个人使用项目,Unladen Swallow似乎很有趣,谢谢提示 :) - mejiwa

0
使用列表推导式来完成你想做的事情将是更符合Python风格的方式。尽管会有性能损失 :)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接