python - for - ¿Por qué la comprensión de una lista es mucho más rápida que agregarla a una lista?
python list comprehension if (3)
Citando
this
artículo, se debe a que el atributo
append
de la
list
no se busca, carga y llama como una función, lo que lleva tiempo y se suma a las iteraciones.
Me preguntaba por qué la comprensión de la lista es mucho más rápida que agregarla a una lista. Pensé que la diferencia es simplemente expresiva, pero no lo es.
>>> import timeit
>>> timeit.timeit(stmt=''''''/
t = []
for i in range(10000):
t.append(i)'''''', number=10000)
9.467898777974142
>>> timeit.timeit(stmt=''t= [i for i in range(10000)]'', number=10000)
4.1138417314859
La comprensión de la lista es un 50% más rápida. ¿Por qué?
Incluso teniendo en cuenta el tiempo que lleva buscar y cargar la función de
append
, la comprensión de la lista es aún más rápida porque la lista se crea en C, en lugar de crear un elemento a la vez en Python.
# Slow
timeit.timeit(stmt=''''''
for i in range(10000):
t.append(i)'''''', setup=''t=[]'', number=10000)
# Faster
timeit.timeit(stmt=''''''
for i in range(10000):
l(i)'''''', setup=''t=[]; l=t.append'', number=10000)
# Faster still
timeit.timeit(stmt=''t = [i for i in range(10000)]'', number=10000)
La comprensión de la lista
es básicamente un "azúcar sintáctico" para el ciclo
for
regular.
En este caso, la razón por la que funciona mejor es porque no necesita cargar el atributo append de la lista y llamarlo como una función en cada iteración.
En otras palabras y
en general
, las comprensiones de listas funcionan más rápido porque suspender y reanudar el marco de una función, o múltiples funciones en otros casos, es más lento que crear una lista a pedido.
Considere los siguientes ejemplos:
# Python-3.6
In [1]: import dis
In [2]: def f1():
...: l = []
...: for i in range(5):
...: l.append(i)
...:
In [3]: def f2():
...: [i for i in range(5)]
...:
In [4]: dis.dis(f1)
2 0 BUILD_LIST 0
3 STORE_FAST 0 (l)
3 6 SETUP_LOOP 33 (to 42)
9 LOAD_GLOBAL 0 (range)
12 LOAD_CONST 1 (5)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
>> 19 FOR_ITER 19 (to 41)
22 STORE_FAST 1 (i)
4 25 LOAD_FAST 0 (l)
28 LOAD_ATTR 1 (append)
31 LOAD_FAST 1 (i)
34 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
37 POP_TOP
38 JUMP_ABSOLUTE 19
>> 41 POP_BLOCK
>> 42 LOAD_CONST 0 (None)
45 RETURN_VALUE
In [5]: dis.dis(f2)
2 0 LOAD_CONST 1 (<code object <listcomp> at 0x7fe48b2265d0, file "<ipython-input-3-9bc091d521d5>", line 2>)
3 LOAD_CONST 2 (''f2.<locals>.<listcomp>'')
6 MAKE_FUNCTION 0
9 LOAD_GLOBAL 0 (range)
12 LOAD_CONST 3 (5)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 POP_TOP
23 LOAD_CONST 0 (None)
26 RETURN_VALUE
Puede ver que en el desplazamiento 22 tenemos un atributo
append
en la primera función, ya que no tenemos tal cosa en la segunda función usando la comprensión de la lista.
Todos esos códigos de bytes adicionales harán que el enfoque de anexión sea más lento.
También tenga en cuenta que también tendrá que cargar el atributo
append
en cada iteración, lo que hace que su código tarde aproximadamente 2 veces más lento que la segunda función que utiliza la comprensión de la lista.