Скажите, что у меня есть функция 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)
ПЕРЕИЗДАЙТЕ: Удаленная вводящая в заблуждение информация от предыдущего редактирования.
Кто-то написал: """Python достаточно умен. Пока у объекта, над которым вы выполняете итерацию, есть метод __len__
или __length_hint__
, Python вызовет его для определения размера и предварительного распределения массива."""
Насколько я могу судить, в list comprehension нет предварительного распределения. У 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
>>>
Это все тот же старый прием: начните с построения пустого списка, затем итерируйте итерабельную таблицу, добавляя к списку по мере необходимости. Я не вижу здесь никакого предварительного распределения.
Оптимизация, о которой вы думаете, используется внутри одного опкода, например, реализация list.extend(iterable)
может предварительно распределяться, если iterable
может точно сообщить свою длину. list.append(object)
дается один объект, а не итерабельность.
Если вы используете модуль 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
Обратите внимание, что в отличие от ответа Хавьера, я включаю mylist = [None] * N
как часть кода timeit is to time при использовании метода «предварительного распределения». (Это не просто часть настройки, поскольку это код, который может понадобиться только при использовании предварительного распределения.)
PS. модуль time
(и команда time
(unix)) может дать недостоверные результаты . Если вы хотите синхронизировать код Python, я бы посоветовал использовать модуль timeit
.
Здесь придется не соглашаться с Хавьером ...
Используя следующий код:
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
Из этой таблицы видно, что понимание списка происходит быстрее, чем предварительное распределение в каждом случай этих различной длины.
Интересный вопрос. Что касается следующего теста, кажется, что предварительное выделение не улучшает производительность в текущей реализации 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
(которое я бы рекомендовал) и другими решениями. Если позже вы не обработаете все элементы списка, вы также можете использовать ленивый метод (показанный в приведенном выше коде), который создаст генератор, который производит элементы к тому времени, когда вы выполняете итерацию по нему.
Использование понимания списка для выполнения того, что вы пытаетесь сделать, было бы более питоническим способом сделать это. Несмотря на снижение производительности :).
Нет никакой разницы в вычислительной сложности между использованием массива с авторазмером и предварительным распределением массива. В худшем случае, это стоит примерно O(2N). См. здесь:
Постоянное амортизированное время
Стоимость вызовов функций плюс все, что происходит в вашей функции, сделает это дополнительное n несущественным. Это не то, о чем вы должны беспокоиться. Просто используйте понимание списка.