Python >> Tutoriel Python >  >> Python

Les tuples sont-ils plus efficaces que les listes en Python ?

Résumé

Les tuples ont tendance à être plus performants que les listes dans presque toutes les catégories :

1) Les tuples peuvent être pliés en permanence.

2) Les tuples peuvent être réutilisés au lieu d'être copiés.

3) Les tuples sont compacts et ne sont pas suralloués.

4) Les tuples référencent directement leurs éléments.

Les tuples peuvent être pliés en permanence

Les tuples de constantes peuvent être précalculés par l'optimiseur de judas de Python ou l'optimiseur AST. Les listes, en revanche, sont créées à partir de rien :

    >>> from dis import dis

    >>> dis(compile("(10, 'abc')", '', 'eval'))
      1           0 LOAD_CONST               2 ((10, 'abc'))
                  3 RETURN_VALUE   

    >>> dis(compile("[10, 'abc']", '', 'eval'))
      1           0 LOAD_CONST               0 (10)
                  3 LOAD_CONST               1 ('abc')
                  6 BUILD_LIST               2
                  9 RETURN_VALUE 

Les tuples n'ont pas besoin d'être copiés

Exécution de tuple(some_tuple) revient immédiatement lui-même. Comme les tuples sont immuables, ils n'ont pas besoin d'être copiés :

>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True

En revanche, list(some_list) nécessite que toutes les données soient copiées dans une nouvelle liste :

>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False

Les tuples ne sur-allouent pas

Étant donné que la taille d'un tuple est fixe, il peut être stocké de manière plus compacte que les listes qui doivent sur-allouer pour faire append() opérations efficaces.

Cela donne aux tuples un bel avantage d'espace :

>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

Voici le commentaire de Objects/listobject.c qui explique ce que font les listes :

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 * Note: new_allocated won't overflow because the largest possible value
 *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
 */

Les tuples font directement référence à leurs éléments

Les références aux objets sont incorporées directement dans un objet tuple. En revanche, les listes ont une couche supplémentaire d'indirection vers un tableau externe de pointeurs.

Cela donne aux tuples un petit avantage de vitesse pour les recherches indexées et le déballage :

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'a[1]'
10000000 loops, best of 3: 0.0304 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'a[1]'
10000000 loops, best of 3: 0.0309 usec per loop

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'x, y, z = a'
10000000 loops, best of 3: 0.0249 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'x, y, z = a'
10000000 loops, best of 3: 0.0251 usec per loop

Voici comment le tuple (10, 20) est stocké :

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject *ob_item[2];     /* store a pointer to 10 and a pointer to 20 */
    } PyTupleObject;

Voici comment la liste [10, 20] est stocké :

    PyObject arr[2];              /* store a pointer to 10 and a pointer to 20 */

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject **ob_item = arr; /* store a pointer to the two-pointer array */
        Py_ssize_t allocated;
    } PyListObject;

Notez que l'objet tuple incorpore directement les deux pointeurs de données tandis que l'objet liste a une couche supplémentaire d'indirection vers un tableau externe contenant les deux pointeurs de données.


En général, vous pouvez vous attendre à ce que les tuples soient légèrement plus rapides. Cependant, vous devez absolument tester votre cas spécifique (si la différence peut avoir un impact sur les performances de votre programme - rappelez-vous "l'optimisation prématurée est la racine de tous les maux").

Python rend cela très simple :timeit est votre ami.

$ python -m timeit "x=(1,2,3,4,5,6,7,8)"
10000000 loops, best of 3: 0.0388 usec per loop

$ python -m timeit "x=[1,2,3,4,5,6,7,8]"
1000000 loops, best of 3: 0.363 usec per loop

et...

$ python -m timeit -s "x=(1,2,3,4,5,6,7,8)" "y=x[3]"
10000000 loops, best of 3: 0.0938 usec per loop

$ python -m timeit -s "x=[1,2,3,4,5,6,7,8]" "y=x[3]"
10000000 loops, best of 3: 0.0649 usec per loop

Donc, dans ce cas, l'instanciation est presque d'un ordre de grandeur plus rapide pour le tuple, mais l'accès aux éléments est en fait un peu plus rapide pour la liste ! Donc, si vous créez quelques tuples et que vous y accédez plusieurs fois, il peut en fait être plus rapide d'utiliser des listes à la place.

Bien sûr, si vous voulez changer un élément, la liste sera certainement plus rapide puisque vous devrez créer un nouveau tuple entier pour en changer un élément (puisque les tuples sont immuables).


Le dis module désassemble le code d'octet d'une fonction et est utile pour voir la différence entre les tuples et les listes.

Dans ce cas, vous pouvez voir que l'accès à un élément génère du code identique, mais que l'affectation d'un tuple est beaucoup plus rapide que l'affectation d'une liste.

>>> def a():
...     x=[1,2,3,4,5]
...     y=x[2]
...
>>> def b():
...     x=(1,2,3,4,5)
...     y=x[2]
...
>>> import dis
>>> dis.dis(a)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 LOAD_CONST               4 (4)
             12 LOAD_CONST               5 (5)
             15 BUILD_LIST               5
             18 STORE_FAST               0 (x)

  3          21 LOAD_FAST                0 (x)
             24 LOAD_CONST               2 (2)
             27 BINARY_SUBSCR
             28 STORE_FAST               1 (y)
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE
>>> dis.dis(b)
  2           0 LOAD_CONST               6 ((1, 2, 3, 4, 5))
              3 STORE_FAST               0 (x)

  3           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 (2)
             12 BINARY_SUBSCR
             13 STORE_FAST               1 (y)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE