Python consume mucha memoria o ¿cómo reducir el tamaño de los objetos?

Puede surgir un problema de memoria cuando necesita tener una gran cantidad de objetos durante la ejecución del programa, especialmente si hay restricciones en el tamaño total de la RAM disponible.


La siguiente es una descripción general de algunos métodos para reducir el tamaño de los objetos, que pueden reducir significativamente la cantidad de RAM necesaria para los programas en Python puro.


Para simplificar, consideraremos estructuras en Python para representar puntos con coordenadas x , y , z con acceso a los valores de coordenadas por nombre.


Dict


En programas pequeños, especialmente scripts, es bastante simple y conveniente usar el dict incorporado para representar información estructural:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

Con el advenimiento de una implementación más "compacta" en Python 3.6 con un conjunto ordenado de claves, dict vuelto aún más atractivo. Sin embargo, mire el tamaño de su rastro en RAM:


 >>> print(sys.getsizeof(ob)) 240 

Requiere mucha memoria, especialmente si de repente necesitas crear una gran cantidad de instancias:


Numero de copiasTamaño de la traza
1,000,000240 Mb
10,000,0002,40 GB
100,000,00024 GB

Instancia de clase


Para aquellos a quienes les gusta vestir todo en clases, es preferible definirlo como una clase con acceso por nombre de atributo:


 class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y 

La estructura de la instancia de clase es interesante:


El campoTamaño (bytes)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL56

Aquí __weakref__ es un enlace a una lista de las llamadas referencias débiles a este objeto, el campo __dict__ es un enlace a un diccionario de una instancia de una clase que contiene valores de atributos de instancia (tenga en cuenta que los enlaces en una plataforma de 64 bits ocupan 8 bytes). Comenzando con Python 3.3, se usa un espacio de clave de diccionario compartido para todas las instancias de la clase. Esto reduce el tamaño del rastreo de instancia en la memoria:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

Como resultado, una gran cantidad de instancias de clase dejan una huella más pequeña en la memoria que un diccionario normal ( dict ):


Numero de copiasTamaño de la traza
1,000,000168 Mb
10,000,0001,68 GB
100,000,00016,8 GB

Es fácil ver que el rastro de la instancia en la memoria todavía es grande debido al tamaño del diccionario de la instancia.


Instancia de clase con __slots__


Una reducción significativa en el rastro de una instancia en la memoria se logra al eliminar __dict__ y __weakref__ . Esto es posible con el "truco" con __slots__ :


 class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64 

La huella en la memoria se ha vuelto mucho más compacta:


El campoTamaño (bytes)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL64

El uso de __slots__ en la definición de clase lleva al hecho de que el rastro de una gran cantidad de instancias en la memoria se reduce significativamente:


Numero de copiasTamaño de la traza
1,000,00064 Mb
10,000,000640 Mb
100,000,0006.4 GB

Actualmente, este es el método principal para reducir significativamente el rastro de una instancia de clase en la memoria del programa.


Esta reducción se logra por el hecho de que después del encabezado del objeto, las referencias a los objetos se almacenan en la memoria, y el acceso a ellos se realiza mediante descriptores especiales que se encuentran en el diccionario de la clase:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

Hay una biblioteca con lista de __slots__ para automatizar el proceso de creación de una clase con __slots__ . La función namedlist.namedlist crea una estructura de clase idéntica a la clase con __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

Otro paquete attrs le permite automatizar el proceso de creación de clases con y sin __slots__ .


Tupla


Python también tiene un tipo de tuple incorporado para representar conjuntos de datos. Tuple es una estructura o registro fijo, pero sin nombres de campo. Para acceder al campo, se utiliza el índice del campo. Los campos de tupla están asociados de una vez por todas con los objetos de valor en el momento en que se instancia la tupla:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y #  

Las instancias de tupla son bastante compactas:


 >>> print(sys.getsizeof(ob)) 72 

Ocupan 8 bytes más en la memoria que las instancias de clase con __slots__ , ya que la traza de tuplas en la memoria también contiene el número de campos:


El campoTamaño (bytes)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL72

Namedtuple


Dado que la tupla se usa ampliamente, un día hubo una solicitud para poder tener acceso a los campos por su nombre también. La respuesta a esta solicitud fue el módulo collections.namedtuple .


La función namedtuple diseñada para automatizar el proceso de generación de estas clases:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

Crea una subclase de tupla, que define los identificadores para acceder a los campos por nombre. Para nuestro ejemplo, se verá más o menos así:


  class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

Todas las instancias de tales clases tienen una traza en memoria idéntica a la tupla. Un gran número de instancias deja una huella de memoria ligeramente mayor:


Numero de copiasTamaño de la traza
1,000,00072 Mb
10,000,000720 Mb

Recordclass: mutated namedtuple sin GC


Dado que las clases tuple y, en consecuencia, namedtuple generan objetos no mutables en el sentido de que un objeto de valor ob.x ya no puede asociarse con otro objeto de valor, ha surgido una solicitud de una variante namedtuple mutada. Dado que Python no tiene un tipo incorporado que sea idéntico a una tupla que admita asignaciones, se han creado muchas variaciones. Nos centraremos en la clase récord , que recibió una calificación de stackoverflow . Además, se puede usar para reducir el tamaño de la traza de un objeto en la memoria en comparación con el tamaño de una traza de objetos del tipo tuple .


En el paquete recordclass , se introduce el tipo recordclass.mutabletuple , que es casi idéntico a tuple pero también admite asignaciones. Sobre esta base, se crean subclases que son casi idénticas a las tuplas con nombre, pero también admiten la asignación de nuevos valores a los campos (sin crear nuevas instancias). La función recordclass , como la función namedtuple , automatiza la creación de tales clases:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

Las instancias de la clase tienen la misma estructura que las tuple , pero solo sin PyGC_Head :


El campoTamaño (bytes)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL48

Por defecto, la función recordclass genera una clase que no está involucrada en el mecanismo de recolección de basura circular. Típicamente, namedtuple y recordclass se usan para generar clases que representan registros o estructuras de datos simples (no recursivas). Su uso correcto en Python no genera referencias circulares. Por esta razón, el seguimiento de instancias de clases generadas por la PyGC_Head predeterminada PyGC_Head fragmento PyGC_Head , que es necesario para las clases que admiten el mecanismo de recolección de basura cíclica (más precisamente: el indicador PyTypeObject no está establecido en el campo de flags en la estructura PyTypeObject correspondiente a la clase que se está creando).


El tamaño de rastreo de una gran cantidad de instancias es menor que el de las instancias de una clase con __slots__ :


Numero de copiasTamaño de la traza
1,000,00048 Mb
10,000,000480 Mb
100,000,0004.8 GB

Objeto de datos


Otra solución propuesta en la biblioteca de recordclass basa en la idea: usar la estructura de almacenamiento en la memoria, como en las instancias de clases con __slots__ , pero no participar en el mecanismo de recolección de basura cíclica. La clase se recordclass.make_dataclass utilizando la función recordclass.make_dataclass :


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

La clase predeterminada creada de esta manera crea instancias mutadas.


Otra forma es usar la declaración de clase heredando de recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

Las clases creadas de esta manera generarán instancias que no participan en el mecanismo de recolección de basura circular. La estructura de la instancia en la memoria es la misma que con __slots__ , pero sin el encabezado PyGC_Head :


El campoTamaño (bytes)
PyObject_HEAD16
x8
y8
y8
TOTAL40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

Para acceder a los campos, los descriptores especiales también se utilizan para acceder al campo por su desplazamiento relativo al comienzo del objeto, que se colocan en el diccionario de la clase:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

El tamaño de rastreo de una gran cantidad de instancias es el más pequeño posible para CPython:


Numero de copiasTamaño de la traza
1,000,00040 Mb
10,000,000400 Mb
100,000,0004.0 GB

Cython


Hay un enfoque basado en el uso de Cython . Su ventaja es que los campos pueden tomar valores de tipos de lenguaje C. Los descriptores para acceder a los campos desde Python puro se crean automáticamente. Por ejemplo:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

En este caso, las instancias tienen un tamaño de memoria aún menor:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

Un rastreo de instancia en memoria tiene la siguiente estructura:


El campoTamaño (bytes)
PyObject_HEAD16
x4 4
y4 4
y4 4
esta vacio4 4
TOTAL32

El tamaño de rastreo de una gran cantidad de copias es menor:


Numero de copiasTamaño de la traza
1,000,00032 Mb
10,000,000320 Mb
100,000,0003,2 GB

Sin embargo, debe recordarse que al acceder desde el código Python, la conversión de objetos int a Python y viceversa se realizará cada vez.


Numpy


El uso de matrices multidimensionales o de registros para grandes cantidades de datos proporciona una ganancia en memoria. Sin embargo, para un procesamiento eficiente en Python puro, debe usar métodos de procesamiento que se centren en usar funciones del paquete numpy .


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

Una matriz y N elementos inicializados con ceros se crean usando la función:


  >>> points = numpy.zeros(N, dtype=Point) 

El tamaño de la matriz es el más pequeño posible:


Numero de copiasTamaño de la traza
1,000,00012 Mb
10,000,000120 Mb
100,000,0001.20 GB

El acceso regular a elementos de matriz y cadenas requerirá una conversión de objeto Python
en el valor de C int y viceversa. La extracción de una sola fila da como resultado una matriz que contiene un único elemento. Su pista no será tan compacta:


  >>> sys.getsizeof(points[0]) 68 

Por lo tanto, como se señaló anteriormente, en el código Python, es necesario procesar matrices usando funciones del paquete numpy .


Conclusión


Utilizando un ejemplo claro y simple, fue posible verificar que la comunidad de desarrolladores y usuarios del lenguaje de programación Python (CPython) tenía oportunidades reales para reducir significativamente la cantidad de memoria utilizada por los objetos.

Source: https://habr.com/ru/post/455722/


All Articles