A tuple
ocupa menos espacio de memoria en Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
mientras que list
s ocupa más espacio en la memoria:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
¿Qué sucede internamente en la gestión de memoria de Python?
A tuple
ocupa menos espacio de memoria en Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
mientras que list
s ocupa más espacio en la memoria:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
¿Qué sucede internamente en la gestión de memoria de Python?
Respuestas:
Supongo que está usando CPython y con 64 bits (obtuve los mismos resultados en mi CPython 2.7 de 64 bits). Puede haber diferencias en otras implementaciones de Python o si tiene un Python de 32 bits.
Independientemente de la implementación, list
los mensajes de correo electrónico son de tamaño variable mientras quetuple
s son de tamaño fijo.
Entonces tuple
s pueden almacenar los elementos directamente dentro de la estructura, las listas, por otro lado, necesitan una capa de indirección (almacena un puntero a los elementos). Esta capa de indirección es un puntero, en sistemas de 64 bits que son 64 bits, por lo tanto, 8 bytes.
Pero hay otra cosa que list
sí: sobreasignan. De list.append
lo contrario , sería una O(n)
operación siempre : para que se amortice O(1)
(¡mucho más rápido!), Se sobreasigna. Pero ahora tiene que realizar un seguimiento del tamaño asignado y el tamaño de relleno (tuple
solo es necesario almacenar un tamaño, porque el tamaño asignado y el tamaño de relleno son siempre idénticos). Eso significa que cada lista tiene que almacenar otro "tamaño" que en los sistemas de 64 bits es un número entero de 64 bits, de nuevo 8 bytes.
Entonces, los list
s necesitan al menos 16 bytes más de memoria que los tuple
s. ¿Por qué dije "al menos"? Debido a la sobreasignación. La sobreasignación significa que asigna más espacio del necesario. Sin embargo, la cantidad de sobreasignación depende de "cómo" crea la lista y el historial de anexos / eliminaciones:
>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4) # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1) # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2) # no re-alloc
>>> l.append(3) # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4) # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72
Decidí crear algunas imágenes para acompañar la explicación anterior. Quizás estos sean útiles
Así es como (esquemáticamente) se almacena en la memoria en su ejemplo. Destaqué las diferencias con los ciclos rojos (a mano alzada):
En realidad, eso es solo una aproximación porque los int
objetos también son objetos de Python y CPython incluso reutiliza números enteros pequeños, por lo que una representación probablemente más precisa (aunque no tan legible) de los objetos en la memoria sería:
Enlaces útiles:
tuple
estructura en el repositorio CPython para Python 2.7list
estructura en el repositorio CPython para Python 2.7int
estructura en el repositorio CPython para Python 2.7¡Tenga en cuenta que __sizeof__
realmente no devuelve el tamaño "correcto"! Solo devuelve el tamaño de los valores almacenados. Sin embargo, cuando usa sys.getsizeof
el resultado es diferente:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
Hay 24 bytes "extra". Estos son reales , esa es la sobrecarga del recolector de basura que no se tiene en cuenta en el __sizeof__
método. Esto se debe a que generalmente no se supone que use métodos mágicos directamente; use las funciones que saben cómo manejarlos, en este caso: sys.getsizeof
(que en realidad agrega la sobrecarga de GC al valor devuelto __sizeof__
).
list
la asignación de memoria stackoverflow.com/questions/40018398/…
list()
o una lista de comprensión.
Profundizaré en el código base de CPython para que podamos ver cómo se calculan realmente los tamaños. En su ejemplo específico , no se han realizado sobreasignaciones, así que no tocaré eso .
Voy a usar valores de 64 bits aquí, como tú.
El tamaño de list
s se calcula a partir de la siguiente función,list_sizeof
:
static PyObject *
list_sizeof(PyListObject *self)
{
Py_ssize_t res;
res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
return PyInt_FromSsize_t(res);
}
Aquí Py_TYPE(self)
hay una macro que toma el ob_type
de self
(regresando PyList_Type
) mientras que _PyObject_SIZE
es otra macro que toma tp_basicsize
de ese tipo. tp_basicsize
se calcula como sizeof(PyListObject)
dónde PyListObject
está la estructura de la instancia.
La PyListObject
estructura tiene tres campos:
PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
estos tienen comentarios (que recorté) que explican qué son, siga el enlace de arriba para leerlos. PyObject_VAR_HEAD
se expande en tres campos de 8 bytes ( ob_refcount
, ob_type
y ob_size
) por lo que es una 24
contribución de bytes.
Entonces por ahora res
es:
sizeof(PyListObject) + self->allocated * sizeof(void*)
o:
40 + self->allocated * sizeof(void*)
Si la instancia de la lista tiene elementos asignados. la segunda parte calcula su contribución. self->allocated
, como su nombre lo indica, contiene el número de elementos asignados.
Sin ningún elemento, el tamaño de las listas se calcula como:
>>> [].__sizeof__()
40
es decir, el tamaño de la estructura de la instancia.
tuple
los objetos no definen una tuple_sizeof
función. En cambio, usan object_sizeof
para calcular su tamaño:
static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
Py_ssize_t res, isize;
res = 0;
isize = self->ob_type->tp_itemsize;
if (isize > 0)
res = Py_SIZE(self) * isize;
res += self->ob_type->tp_basicsize;
return PyInt_FromSsize_t(res);
}
Esto, en cuanto a list
s, toma el tp_basicsize
y, si el objeto tiene un valor distinto de cero tp_itemsize
(lo que significa que tiene instancias de longitud variable), multiplica el número de elementos en la tupla (que obtiene a través de Py_SIZE
) contp_itemsize
.
tp_basicsize
nuevamente usa sizeof(PyTupleObject)
donde la PyTupleObject
estructura contiene :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Entonces, sin ningún elemento (es decir, Py_SIZE
devoluciones 0
), el tamaño de las tuplas vacías es igual a sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
eh Bueno, aquí hay una rareza para la que no he encontrado una explicación, la tp_basicsize
de tuple
s se calcula de la siguiente manera:
sizeof(PyTupleObject) - sizeof(PyObject *)
por qué 8
se eliminan bytes adicionales tp_basicsize
es algo que no he podido averiguar. (Ver el comentario de MSeifert para una posible explicación)
Pero, esta es básicamente la diferencia en su ejemplo específico . list
s también mantienen una serie de elementos asignados que ayuda a determinar cuándo sobreasignar nuevamente.
Ahora, cuando se agregan elementos adicionales, las listas sí realizan esta sobreasignación para lograr agregados O (1). Esto da como resultado tamaños más grandes, ya que MSeifert cubre muy bien en su respuesta.
ob_item[1]
es principalmente un marcador de posición (por lo que tiene sentido que se reste del tamaño básico). El tuple
se asigna usando PyObject_NewVar
. No he descubierto los detalles, así que es solo una suposición
La respuesta de MSeifert lo cubre ampliamente; para hacerlo simple, puede pensar en:
tuple
es inmutable. Una vez configurado, no puede cambiarlo. Entonces, sabe de antemano cuánta memoria necesita asignar para ese objeto.
list
es mutable. Puede agregar o quitar elementos de él. Tiene que saber su tamaño (para impl. Interna). Cambia de tamaño según sea necesario.
No hay comidas gratis ; estas capacidades tienen un costo. De ahí la sobrecarga en la memoria para las listas.
El tamaño de la tupla tiene un prefijo, lo que significa que en la inicialización de la tupla el intérprete asigna suficiente espacio para los datos contenidos, y ese es el final, lo que le da inmutable (no se puede modificar), mientras que una lista es un objeto mutable, por lo tanto, implica dinámica asignación de memoria, por lo que para evitar asignar espacio cada vez que agrega o modifica la lista (asigne suficiente espacio para contener los datos cambiados y copie los datos), asigna espacio adicional para futuras adiciones, modificaciones, ... eso prácticamente lo resume.