假设我有一个函数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)
重新编辑:删除了之前编辑中的误导性信息。
答案 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)。见这里:
函数调用的成本加上函数中发生的任何事情都会使这个额外的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的方式来做到这一点。尽管性能损失:)。