Python 3: самый эффективный способ создать [func (i), поскольку я в диапазоне (N)] перечисляю понимание

Скажите, что у меня есть функция 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)

ПЕРЕИЗДАЙТЕ: Удаленная вводящая в заблуждение информация от предыдущего редактирования.

7
задан mejiwa 14 March 2010 в 14:48
поделиться

6 ответов

Кто-то написал: """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) дается один объект, а не итерабельность.

7
ответ дан 6 December 2019 в 19:35
поделиться

Если вы используете модуль 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 .

2
ответ дан 6 December 2019 в 19:35
поделиться

Здесь придется не соглашаться с Хавьером ...

Используя следующий код:

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
ответ дан 6 December 2019 в 19:35
поделиться

Интересный вопрос. Что касается следующего теста, кажется, что предварительное выделение не улучшает производительность в текущей реализации 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 (которое я бы рекомендовал) и другими решениями. Если позже вы не обработаете все элементы списка, вы также можете использовать ленивый метод (показанный в приведенном выше коде), который создаст генератор, который производит элементы к тому времени, когда вы выполняете итерацию по нему.

1
ответ дан 6 December 2019 в 19:35
поделиться

Использование понимания списка для выполнения того, что вы пытаетесь сделать, было бы более питоническим способом сделать это. Несмотря на снижение производительности :).

0
ответ дан 6 December 2019 в 19:35
поделиться

Нет никакой разницы в вычислительной сложности между использованием массива с авторазмером и предварительным распределением массива. В худшем случае, это стоит примерно O(2N). См. здесь:

Постоянное амортизированное время

Стоимость вызовов функций плюс все, что происходит в вашей функции, сделает это дополнительное n несущественным. Это не то, о чем вы должны беспокоиться. Просто используйте понимание списка.

2
ответ дан 6 December 2019 в 19:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: