Creé dos listas l1
y l2
, pero cada una con un método de creación diferente:
import sys
l1 = [None] * 10
l2 = [None for _ in range(10)]
print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))
Pero la salida me sorprendió:
Size of l1 = 144
Size of l2 = 192
La lista creada con una comprensión de la lista es de mayor tamaño en la memoria, pero las dos listas son idénticas en Python de lo contrario.
¿Porqué es eso? ¿Es esto algo interno de CPython, o alguna otra explicación?
python
list
memory-management
python-internals
Andrej Kesely
fuente
fuente
144 == sys.getsizeof([]) + 8*10)
donde 8 es el tamaño de un puntero.10
a11
, la[None] * 11
lista tiene tamaño152
, pero la comprensión de la lista todavía tiene tamaño192
. La pregunta vinculada anteriormente no es un duplicado exacto, pero es relevante para entender por qué sucede esto.Respuestas:
Cuando escribe
[None] * 10
, Python sabe que necesitará una lista de exactamente 10 objetos, por lo que asigna exactamente eso.Cuando utiliza una lista de comprensión, Python no sabe cuánto necesitará. Por lo tanto, gradualmente crece la lista a medida que se agregan elementos. Para cada reasignación, asigna más espacio del que se necesita inmediatamente, de modo que no tiene que reasignarse para cada elemento. Es probable que la lista resultante sea algo más grande de lo necesario.
Puede ver este comportamiento al comparar listas creadas con tamaños similares:
Puede ver que el primer método asigna exactamente lo que se necesita, mientras que el segundo crece periódicamente. En este ejemplo, asigna suficiente para 16 elementos, y tuvo que reasignarse al llegar al 17.
fuente
*
cuando sepa el tamaño al frente.[x] * n
con inmutablex
en su lista. La lista resultante contendrá referencias al objeto idéntico.Como se señaló en esta pregunta, la comprensión de la lista se utiliza
list.append
debajo del capó, por lo que llamará al método de cambio de tamaño de la lista, que se sobreasigna.Para demostrarte esto a ti mismo, puedes usar el
dis
desensamblador:Observe el
LIST_APPEND
código de operación en el desmontaje del<listcomp>
objeto de código. De los documentos :Ahora, para la operación de repetición de lista, tenemos una pista sobre lo que está sucediendo si consideramos:
Entonces, parece ser capaz de asignar exactamente el tamaño. Mirando el código fuente , vemos que esto es exactamente lo que sucede:
Es decir, aquí:
size = Py_SIZE(a) * n;
. El resto de las funciones simplemente llena la matriz.fuente
.extend()
.list.append
es una operación amortizada de tiempo constante porque cuando una lista cambia de tamaño, se sobreasigna. No todas las operaciones de agregar, por lo tanto, dan como resultado una matriz recién asignada. En cualquier caso, la pregunta a la que he vinculado le muestra en el código fuente que, de hecho, las comprensiones de listas sí usanlist.append
. Regresaré a mi computadora portátil en un momento y puedo mostrarles el bytecode desmontado para una comprensión de la lista y elLIST_APPEND
código de operación correspondienteNinguno es un bloque de memoria, pero no es un tamaño especificado previamente. Además de eso, hay un espacio adicional en una matriz entre los elementos de la matriz. Puede ver esto usted mismo ejecutando:
Lo que no totaliza el tamaño de l2, sino que es menor.
Y esto es mucho mayor que una décima parte del tamaño de
l1
.Sus números deben variar según los detalles de su sistema operativo y los detalles del uso de memoria actual en su sistema operativo. El tamaño de [Ninguno] nunca puede ser mayor que la memoria adyacente disponible donde la variable está configurada para ser almacenada, y la variable puede tener que moverse si luego se asigna dinámicamente para que sea más grande.
fuente
None
en realidad no se almacena en la matriz subyacente, lo único que se almacena es unPyObject
puntero (8 bytes). Todos los objetos de Python se asignan en el montón.None
es un singleton, por lo que tener una lista con muchos nones simplemente creará una matriz de punteros PyObject para el mismoNone
objeto en el montón (y no usará memoria adicional en el proceso por cada adicionalNone
). No estoy seguro de lo que quiere decir con "Ninguno no tiene un tamaño predeterminado", pero eso no suena correcto. Finalmente, su ciclo congetsizeof
cada elemento no demuestra lo que parece pensar que está demostrando.gestsizeof
de cada unoele
de losl2
induce a error porquegetsizeof(l2)
no tiene en cuenta el tamaño de los elementos en el interior del contenedor .l1 = [None]; l2 = [None]*100; l3 = [l2]
a continuaciónprint(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3))
. obtendrá un resultado como:72 864 72
. Eso es, respectivamente,64 + 1*8
,64 + 100*8
, y64 + 1*8
, de nuevo, suponiendo un sistema de 64 bits con 8 bytes tamaño del puntero.sys.getsizeof
* no tiene en cuenta el tamaño de los artículos en el contenedor. De los documentos : "Solo se contabiliza el consumo de memoria directamente atribuido al objeto, no el consumo de memoria de los objetos a los que se refiere ... Vea el tamaño recursivo de la receta para ver un ejemplo del uso de getsizeof () recursivamente para encontrar el tamaño de los contenedores y todos sus contenidos ".