web-dev-qa-db-fra.com

Pourquoi la compréhension d'une liste est-elle tellement plus rapide que l'ajout à une liste?

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?

35
rafaelc

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.

67
Kasrâmvd

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)
12
chepner

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.

6
adarsh