¿Son las tuplas más eficientes que las listas en Python?

Respuestas:

172

El dismódulo desmonta el código de bytes para una función y es útil para ver la diferencia entre tuplas y listas.

En este caso, puede ver que acceder a un elemento genera un código idéntico, pero que asignar una tupla es mucho más rápido que asignar una lista.

>>> 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
Mark Harrison
fuente
66
Err, solo que el mismo bytecode se genere absolutamente no significa que las mismas operaciones sucedan en el nivel C (y, por lo tanto, cpu). Intenta crear una clase ListLikecon algo __getitem__que haga algo terriblemente lento, luego desarma x = ListLike((1, 2, 3, 4, 5)); y = x[2]. El código de bytes será más parecido al ejemplo de tupla anterior que al ejemplo de la lista, pero ¿realmente cree que eso significa que el rendimiento será similar?
mzz
2
Parece que estás diciendo que algunos tipos son más eficientes que otros. Eso tiene sentido, pero la sobrecarga de las generaciones de listas y tuplas parece ser ortogonal al tipo de datos involucrado, con la advertencia de que son listas y tuplas del mismo tipo de datos.
Mark Harrison
11
El número de códigos de bytes, como el número de líneas de código, tiene poca relación con la velocidad de ejecución (y, por lo tanto, la eficiencia y el rendimiento).
Martineau
18
Aunque la sugerencia de que puede concluir cualquier cosa, desde el conteo de operaciones es errónea, esto muestra la diferencia clave: las tuplas constantes se almacenan como tales en el código de bytes y solo se hace referencia a ellas cuando se usan, mientras que las listas deben construirse en tiempo de ejecución.
Poolie
66
Esta respuesta nos muestra que Python reconoce constantes de tupla. ¡Es bueno saberlo! Pero, ¿qué sucede al intentar construir una tupla o una lista a partir de valores variables?
Tom
211

En general, puede esperar que las tuplas sean un poco más rápidas. Sin embargo, definitivamente debe probar su caso específico (si la diferencia puede afectar el rendimiento de su programa, recuerde que "la optimización prematura es la raíz de todo mal").

Python hace esto muy fácil: timeit es su amigo.

$ 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

y...

$ 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

Entonces, en este caso, la instanciación es casi un orden de magnitud más rápido para la tupla, ¡pero el acceso a los elementos es en realidad algo más rápido para la lista! Entonces, si está creando algunas tuplas y accediendo a ellas muchas veces, en realidad puede ser más rápido usar listas.

Por supuesto, si desea cambiar un elemento, la lista definitivamente será más rápida ya que necesitaría crear una tupla completamente nueva para cambiar un elemento (ya que las tuplas son inmutables).

dF.
fuente
3
¡Con qué versión de Python fueron tus pruebas!
Matt Joiner
2
Hay otra prueba interesante - python -m timeit "x=tuple(xrange(999999))"vs python -m timeit "x=list(xrange(999999))". Como cabría esperar, se tarda un poco más en materializar una tupla que una lista.
Hamish Grubijan
3
Parece extraño que el acceso a tuplas sea más lento que el acceso a la lista. Sin embargo, al intentar eso en Python 2.7 en mi PC con Windows 7, la diferencia es solo del 10%, por lo que no es importante.
ToolmakerSteve
51
FWIW, el acceso a la lista es más rápido que el acceso a la tupla en Python 2, pero solo porque hay un caso especial para las listas en BINARY_SUBSCR en Python / ceval.c. En Python 3, esa optimización se ha ido y el acceso a las tuplas se vuelve un poco más rápido que el acceso a la lista.
Raymond Hettinger
3
@yoopoo, la primera prueba crea una lista un millón de veces, pero la segunda crea una lista una vez y accede a ella un millón de veces. El -s "SETUP_CODE"se ejecuta antes del código temporizado real.
leewz
203

Resumen

Las tuplas tienden a funcionar mejor que las listas en casi todas las categorías:

1) Las tuplas pueden doblarse constantemente .

2) Las tuplas pueden reutilizarse en lugar de copiarse.

3) Las tuplas son compactas y no se asignan en exceso.

4) Las tuplas hacen referencia directa a sus elementos.

Las tuplas pueden doblarse constantemente

Las tuplas de constantes pueden calcularse previamente mediante el optimizador de mirilla de Python o el optimizador AST. Las listas, por otro lado, se acumulan desde cero:

    >>> 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 

Las tuplas no necesitan ser copiadas

La ejecución tuple(some_tuple)vuelve inmediatamente a sí misma. Como las tuplas son inmutables, no es necesario copiarlas:

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

Por el contrario, list(some_list)requiere que todos los datos se copien en una nueva lista:

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

Las tuplas no se sobreasignan

Dado que el tamaño de una tupla es fijo, se puede almacenar de manera más compacta que las listas que deben sobreasignarse para que las operaciones append () sean eficientes.

Esto le da a las tuplas una buena ventaja de espacio:

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

Aquí está el comentario de Objects / listobject.c que explica qué están haciendo las listas:

/* 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.
 */

Las tuplas se refieren directamente a sus elementos.

Las referencias a los objetos se incorporan directamente en un objeto de tupla. Por el contrario, las listas tienen una capa adicional de indirección a una matriz externa de punteros.

Esto le da a las tuplas una pequeña ventaja de velocidad para búsquedas indexadas y desempaque:

$ 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

Aquí es cómo la tupla (10, 20)se almacena:

    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;

Aquí es cómo la lista [10, 20]se almacena:

    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;

Tenga en cuenta que el objeto tupla incorpora los dos punteros de datos directamente, mientras que el objeto de la lista tiene una capa adicional de indirección a una matriz externa que contiene los dos punteros de datos.

Raymond Hettinger
fuente
19
Finalmente, alguien pone las estructuras C!
osman
1
Internally, tuples are stored a little more efficiently than lists, and also tuples can be accessed slightly faster. ¿Cómo podrías explicar los resultados de la respuesta de dF.
DRz
55
Al trabajar con ~ 50k listas de ~ 100 listas de elementos, mover esta estructura a tuplas disminuyó los tiempos de búsqueda en múltiples órdenes de magnitud para múltiples búsquedas. Creo que esto se debe a la mayor localidad de caché de la tupla una vez que comienza a usar la tupla debido a la eliminación de la segunda capa de indirección que demuestra.
horta
tuple(some_tuple)solo se devuelve some_tuplesi some_tuplees hashable, cuando su contenido es recursivamente inmutable y hashable. De lo contrario, tuple(some_tuple)devuelve una nueva tupla. Por ejemplo, cuando some_tuplecontiene elementos mutables.
Luciano Ramalho
Las tuplas no siempre son más rápidas. Considere `` `t = () para i en rango (1,100): t + = il = [] para i en rango (1,1000): a.append (i)` `` El segundo es más rápido
melvil james
32

Las tuplas, siendo inmutables, son más eficientes en memoria; enumera, por eficiencia, sobreasignar memoria para permitir anexos sin reallocs constante . Entonces, si desea iterar a través de una secuencia constante de valores en su código (por ejemplo for direction in 'up', 'right', 'down', 'left':), se prefieren las tuplas, ya que dichas tuplas se calculan previamente en tiempo de compilación.

Las velocidades de acceso deben ser las mismas (ambas se almacenan como matrices contiguas en la memoria).

Pero, alist.append(item)se prefiere mucho atuple+= (item,)cuando se trata con datos mutables. Recuerde, las tuplas están destinadas a ser tratadas como registros sin nombres de campo.

tzot
fuente
1
¿Qué es el tiempo de compilación en Python?
balki
1
@balki: el momento en que la fuente de Python se compila en bytecode (que puede guardarse como un archivo .py [co]).
tzot
Una cita sería genial si es posible.
Grijesh Chauhan
9

También debe considerar el arraymódulo en la biblioteca estándar si todos los elementos de su lista o tupla son del mismo tipo C. Tomará menos memoria y puede ser más rápido.

Epónimo
fuente
15
Tomará menos memoria, pero el tiempo de acceso probablemente será un poco más lento, en lugar de más rápido. Acceder a un elemento requiere que el valor empaquetado se desempaquete a un número entero real, lo que ralentizará el proceso.
Brian
5

Aquí hay otro pequeño punto de referencia, solo por el bien ...

In [11]: %timeit list(range(100))
749 ns ± 2.41 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [12]: %timeit tuple(range(100))
781 ns ± 3.34 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [1]: %timeit list(range(1_000))
13.5 µs ± 466 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [2]: %timeit tuple(range(1_000))
12.4 µs ± 182 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [7]: %timeit list(range(10_000))
182 µs ± 810 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [8]: %timeit tuple(range(10_000))
188 µs ± 2.38 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [3]: %timeit list(range(1_00_000))
2.76 ms ± 30.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [4]: %timeit tuple(range(1_00_000))
2.74 ms ± 31.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [10]: %timeit list(range(10_00_000))
28.1 ms ± 266 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [9]: %timeit tuple(range(10_00_000))
28.5 ms ± 447 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Vamos a promediar estos:

In [3]: l = np.array([749 * 10 ** -9, 13.5 * 10 ** -6, 182 * 10 ** -6, 2.76 * 10 ** -3, 28.1 * 10 ** -3])

In [2]: t = np.array([781 * 10 ** -9, 12.4 * 10 ** -6, 188 * 10 ** -6, 2.74 * 10 ** -3, 28.5 * 10 ** -3])

In [11]: np.average(l)
Out[11]: 0.0062112498000000006

In [12]: np.average(t)
Out[12]: 0.0062882362

In [17]: np.average(t) / np.average(l)  * 100
Out[17]: 101.23946713590554

Puedes llamarlo casi inconcluso.

Pero claro, las tuplas se tomaron 101.239%el tiempo o 1.239%tiempo extra para hacer el trabajo en comparación con las listas.

Dev Aggarwal
fuente
4

Las tuplas deberían ser un poco más eficientes y, por eso, más rápidas que las listas porque son inmutables.

ctcherry
fuente
44
¿Por qué dice que la inmutabilidad, en sí misma, aumenta la eficiencia? ¿Especialmente la eficiencia de instanciación y recuperación?
Blair Conrad
1
Parece que la respuesta de Mark sobre la mía ha cubierto las instrucciones desmontadas de lo que sucede dentro de Python. Puede ver que la creación de instancias requiere menos instrucciones, sin embargo, en este caso, la recuperación es aparentemente idéntica.
ctcherry
las tuplas inmutables tienen acceso más rápido que las listas mutables
noobninja
-6

La razón principal para que Tuple sea muy eficiente en la lectura es porque es inmutable.

¿Por qué los objetos inmutables son fáciles de leer?

La razón es que las tuplas se pueden almacenar en la memoria caché, a diferencia de las listas. El programa siempre lee la ubicación de la memoria de las listas, ya que es mutable (puede cambiar en cualquier momento).

Divakar
fuente