Python 3:在范围(N)列表理解中创建[func(i)for i的最有效方法

时间:2010-03-13 20:54:44

标签: python list-comprehension

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

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

不使用像在C中创建函数的高级方法?我对上面列表理解的主要关注是,我不确定python是否事先知道预分配mylist的范围(N)的长度,因此必须逐步重新分配列表。是这样的情况还是python足够聪明,首先将mylist分配给长度N然后计算它的元素?如果没有,创建mylist的最佳方法是什么?也许这个?

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

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

6 个答案:

答案 0 :(得分:7)

有人写道:“”“Python足够聪明。只要您迭代的对象有__len____length_hint__方法,Python就会调用它来确定大小并预先分配阵列 “”“

据我所知,列表理解中没有预分配。 Python无法根据INPUT的大小来判断OUTPUT的大小。

看看这个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
>>>

这是同一个旧的schtick:从构建一个空列表开始,然后迭代迭代,根据需要附加到列表。我在这里看不到预分配。

您正在考虑的优化用于单个操作码,例如如果list.extend(iterable)可以准确报告其长度,iterable的实施可以预先分配。 list.append(object)被赋予一个对象,而不是可迭代的。

答案 1 :(得分:2)

使用自动调整数组和预分配数组之间的计算复杂性没有区别。最糟糕的是,它的成本约为O(2N)。见这里:

Constant Amortized Time

函数调用的成本加上函数中发生的任何事情都会使这个额外的n无关紧要。这不是你应该担心的事情。只需使用列表理解。

答案 2 :(得分:2)

如果你使用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作为代码时间的一部分包含在使用“预分配”方法时。 (它不仅仅是设置的一部分,因为只有在使用预分配时才需要代码。)

PS。 time模块(和time(unix)命令)can give unreliable results。如果您希望计算Python代码的时间,我建议您坚持使用timeit模块。

答案 3 :(得分:1)

在这里不得不同意哈维尔......

使用以下代码:

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

从这张表中可以看出,在每种情况下,这些不同长度的列表理解都比预分配更快。

答案 4 :(得分: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解决方案(我建议)和其他解决方案之间没有太大区别。如果稍后不处理列表中的所有元素,您还可以使用惰性方法(如上面的代码所示),这将创建一个生成元素,在迭代它时生成元素。

答案 5 :(得分:0)

使用列表理解来完成你想要做的事情将是更加pythonic的方式来做到这一点。尽管性能损失:)。