Je me demandais pourquoi la compréhension d'une liste est tellement plus rapide que l'ajout à une liste. Je pensais que la différence est juste expressive, mais ce n'est pas le cas.
>>> 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 compréhension de la liste est 50% plus rapide. Pourquoi?
La compréhension de liste est fondamentalement juste un "sucre syntaxique" pour la boucle régulière de for
. Dans ce cas, la raison pour laquelle il fonctionne mieux est qu'il n'a pas besoin de charger l'attribut append de la liste et de l'appeler en tant que fonction à chaque itération. En d'autres termes et en général, les compréhensions de liste fonctionnent plus rapidement car la suspension et la reprise du cadre d'une fonction, ou de plusieurs fonctions dans d'autres cas, est plus lente que la création d'une liste à la demande.
Considérez les exemples suivants:
# 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
Vous pouvez voir à l'offset 22 que nous avons un attribut append
dans la première fonction puisque nous n'avons pas une telle chose dans la deuxième fonction utilisant la compréhension de liste. Tous ces bytecodes supplémentaires ralentiront l'approche de l'ajout. Notez également que vous aurez également l'attribut append
à chaque itération, ce qui rend votre code environ 2 fois plus lent que la deuxième fonction utilisant la compréhension de liste.
Même en tenant compte du temps qu'il faut pour rechercher et charger la fonction append
, la compréhension de la liste est encore plus rapide car la liste est créée en C, plutôt que construite un élément à la fois 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)
Citer this article, c'est parce que l'attribut append
du list
n'est pas recherché, chargé et appelé en tant que fonction, ce qui prend du temps et qui s'additionne sur les itérations.