valor recorrer lista ejemplo diccionarios diccionario dentro declaracion convertir comparar buscar python list list-comprehension cpython python-internals

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() o list(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