recorrer - lista dentro de un diccionario python
list() usa más memoria que la comprensión list (2)
Así que estaba jugando con objetos de
list
y encontré algo extraño que si la
list
se crea con
list()
, ¿usa más memoria que la comprensión de la lista?
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 docs :
Las listas se pueden construir de varias maneras:
- Usando un par de corchetes para denotar la lista vacía:
[]
- Usando corchetes, separando 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 usando
list()
usa más memoria.
Y a medida que la
list
es mayor, la brecha aumenta.
¿Por qué pasa 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
Creo que está viendo patrones de sobreasignación, 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 las comprensiones de listas 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 asignación excesiva se realiza por motivos de rendimiento, lo que permite que las listas crezcan sin asignar más memoria con cada crecimiento (mejor rendimiento amortized ).
Una razón probable de la diferencia con el uso de la comprensión de la lista es que la comprensión de la lista no puede calcular de manera determinista el tamaño de la lista generada, pero la
list()
puede.
Esto significa que las comprensiones aumentarán continuamente la lista a medida que la llene utilizando una asignación excesiva hasta que finalmente la llene.
Es posible que esto no haga crecer el búfer de sobreasignación con nodos asignados no utilizados una vez hecho (de hecho, en la mayoría de los casos no lo haría, 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
comprensiones de listas invocando
LIST_APPEND
, que indica el uso de
list.resize
, 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ás 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, agotando 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
Gracias a todos por ayudarme a entender esa increíble Python.
No quiero hacer preguntas tan masivas (por eso estoy publicando respuestas), solo quiero mostrar y compartir mis pensamientos.
Como señaló correctamente: "list () determina determinísticamente el tamaño de la lista". Puedes verlo desde ese gráfico.
Cuando
append
o utiliza la comprensión de listas, siempre tiene algún tipo de límites que se extienden cuando llega a algún punto.
Y con
list()
tiene casi los mismos límites, pero están flotando.
ACTUALIZAR
Así que gracias a , tavo , @SvenFestersen
Para resumir:
list()
preasigna memoria depende del tamaño de la lista, la comprensión de la lista no puede hacer eso (solicita más memoria cuando es necesario, como
.append()
).
Es por eso que
list()
almacena más memoria.
Un gráfico más, que muestra
list()
preasigna memoria.
Entonces, la línea verde muestra la
list(range(830))
agregando 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()
debe ser más rápido que la comprensión de la lista, por lo que ejecuté
timeit()
con
number=1000
para la longitud de la
list
de
4**0
a
4**10
y los resultados son