Entonces, estaba jugando con list
objetos y encontré una pequeña cosa extraña que, si list
se crea con list()
, usa más memoria que la comprensión de listas. Estoy usando Python 3.5.2
In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008
De los documentos :
Las listas se pueden construir de varias formas:
- Usando un par de corchetes para denotar la lista vacía:
[]
- El uso de corchetes, que separa los elementos con comas:
[a]
,[a, b, c]
- Usando una lista de comprensión:
[x for x in iterable]
- Usando el constructor de tipos:
list()
olist(iterable)
Pero parece que usarlo list()
consume más memoria.
Y cuanto más list
grande, la brecha aumenta.
¿Por qué sucede esto?
ACTUALIZACIÓN # 1
Prueba con Python 3.6.0b2:
Python 3.6.0b2 (default, Oct 11 2016, 11:52:53)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912
ACTUALIZACIÓN # 2
Prueba con Python 2.7.12:
Python 2.7.12 (default, Jul 1 2016, 15:12:24)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920
python
list
list-comprehension
cpython
python-internals
vishes_shell
fuente
fuente
sys.getsizeof(list(range(100)))
es 1016,getsizeof(range(100))
es 872 ygetsizeof([i for i in range(100)])
es 920. Todos tienen el tipolist
.xrange
.append()
, puede haber una sobreasignación de memoria. Supongo que la única forma de aclarar esto es echar un vistazo a las fuentes de Python.Respuestas:
Creo que está viendo patrones de asignación excesiva, esta es una muestra de la fuente :
/* 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, ... */ new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
Al imprimir los tamaños de listas por comprensión de longitudes 0-88, puede ver las coincidencias de patrones:
# create comprehensions for sizes 0-88 comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)] # only take those that resulted in growth compared to previous length steps = zip(comprehensions, comprehensions[1:]) growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]] # print the results: for growth in growths: print(growth)
Resultados (el formato es
(list length, (old total size, new total size))
):(0, (64, 96)) (4, (96, 128)) (8, (128, 192)) (16, (192, 264)) (25, (264, 344)) (35, (344, 432)) (46, (432, 528)) (58, (528, 640)) (72, (640, 768)) (88, (768, 912))
La sobreasignación se realiza por motivos de rendimiento, lo que permite que las listas crezcan sin asignar más memoria con cada crecimiento (mejor rendimiento amortizado ).
Una razón probable de la diferencia con el uso de la comprensión de listas es que la comprensión de listas no puede calcular de manera determinista el tamaño de la lista generada, pero
list()
sí. Esto significa que las comprensiones harán crecer continuamente la lista a medida que la llene usando una sobreasignación hasta que finalmente la llene.Es posible que no aumente el búfer de sobreasignación con nodos asignados no utilizados una vez que se haya hecho (de hecho, en la mayoría de los casos, eso anularía el propósito de sobreasignación).
list()
, sin embargo, puede agregar algo de búfer sin importar el tamaño de la lista, ya que conoce el tamaño final de la lista de antemano.Otra evidencia de respaldo, también de la fuente, es que vemos la invocación de listas por comprensión
LIST_APPEND
, lo que indica el uso delist.resize
, lo que a su vez indica consumir el búfer de preasignación sin saber cuánto se llenará. Esto es consistente con el comportamiento que está viendo.Para concluir,
list()
preasignará más nodos en función del tamaño de la lista>>> sys.getsizeof(list([1,2,3])) 60 >>> sys.getsizeof(list([1,2,3,4])) 64
La comprensión de la lista no conoce el tamaño de la lista, por lo que utiliza operaciones de adición a medida que crece, lo que agota el búfer de preasignación:
# one item before filling pre-allocation buffer completely >>> sys.getsizeof([i for i in [1,2,3]]) 52 # fills pre-allocation buffer completely # note that size did not change, we still have buffered unused nodes >>> sys.getsizeof([i for i in [1,2,3,4]]) 52 # grows pre-allocation buffer >>> sys.getsizeof([i for i in [1,2,3,4,5]]) 68
fuente
list.resize
. No soy un experto en navegar a través de la fuente, pero si uno llama a resize y el otro no, podría explicar la diferencia.64, 96, 104, 112, 120, 128, 136, 144, 160, 192, 200, 208, 216, 224, 232, 240, 256, 264, 272, 280, 288, 296, 304, 312, 328, 336, 344, 352, 360, 368, 376, 384, 400, 408, 416
y para la comprensión64, 96, 96, 96, 96, 128, 128, 128, 128, 192, 192, 192, 192, 192, 192, 192, 192, 264, 264, 264, 264, 264, 264, 264, 264, 264, 344, 344, 344, 344, 344, 344, 344, 344, 344
. Excepto que la comprensión es la que parece preasignar memoria para ser el algoritmo que usa más RAM para ciertos tamaños.list()
determina de manera determinista el tamaño de la lista, lo que la comprensión de la lista no puede hacer. Esto sugiere que la comprensión de la lista no siempre "desencadena" el "último" crecimiento de la lista. Podría tener sentido.Gracias a todos por ayudarme a comprender ese increíble Python.
No quiero hacer una pregunta tan masiva (por eso estoy publicando la respuesta), solo quiero mostrar y compartir mis pensamientos.
Como @ReutSharabani señaló correctamente: "list () determina determinísticamente el tamaño de la lista". Puedes verlo en ese gráfico.
Cuando usas la
append
comprensión de listas, siempre tienes algún tipo de límites que se extienden cuando llegas a algún punto. Ylist()
contigo tienes casi los mismos límites, pero están flotando.ACTUALIZAR
Así que gracias a @ReutSharabani , @tavo , @SvenFestersen
En resumen: la
list()
memoria preasignada depende del tamaño de la lista, la comprensión de la lista no puede hacer eso (solicita más memoria cuando la necesita, como.append()
). Es por esolist()
almacena más memoria.Un gráfico más, que muestra la
list()
memoria preasignada. Entonces, la línea verde muestra que selist(range(830))
agrega elemento por elemento y durante un tiempo la memoria no cambia.ACTUALIZACIÓN 2
Como @Barmar señaló en los comentarios a continuación,
list()
debo ser más rápido que la comprensión de la lista, así que corrítimeit()
con lanumber=1000
longitud delist
desde4**0
hasta4**10
y los resultados sonfuente
list
constructor puede determinar el tamaño de la nueva lista a partir de su argumento, aún preasignará la misma cantidad de espacio que si el último elemento llegara allí y no hubiera suficiente espacio para él. Al menos eso es lo que tiene sentido para mí.range
objeto (eso podría ser divertido).