Puede surgir un problema de memoria cuando una gran cantidad de objetos están activos en la RAM durante la ejecución de un programa, especialmente si hay restricciones en la cantidad total de memoria disponible.
A continuación se muestra 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.
Nota: Esta es la versión en inglés de mi publicación original (en ruso).
Para simplificar, consideraremos estructuras en Python para representar puntos con las coordenadas x
, y
, z
con acceso a los valores de coordenadas por nombre.
Dict
En programas pequeños, especialmente en 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
ha vuelto aún más atractivo. Sin embargo, veamos el tamaño de su huella en RAM:
>>> print(sys.getsizeof(ob)) 240
Se necesita mucha memoria, especialmente si de repente necesita crear una gran cantidad de instancias:
Instancia de clase
Para aquellos a quienes les gusta vestir todo en clases, es preferible definir estructuras 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:
Aquí __weakref__
es una referencia a la lista de las llamadas referencias débiles a este objeto, el campo __dict__
es una referencia al diccionario de instancia de clase, que contiene los valores de los atributos de instancia (tenga en cuenta que la plataforma de referencias de 64 bits ocupa 8 bytes). A partir de Python 3.3, el espacio compartido se usa para almacenar claves en el diccionario para todas las instancias de la clase. Esto reduce el tamaño del rastreo de instancia en RAM:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
Como resultado, una gran cantidad de instancias de clase tienen una huella más pequeña en la memoria que un diccionario normal ( dict
):
Es fácil ver que el tamaño de la instancia en RAM todavía es grande debido al tamaño del diccionario de la instancia.
Instancia de clase con __slots__
Se logra una reducción significativa en el tamaño de una instancia de clase en RAM al eliminar __dict__
y __weakref__
. Esto es posible con la ayuda de un "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
El tamaño del objeto en RAM se ha vuelto significativamente más pequeño:
El uso de __slots__
en la definición de clase hace que la huella de una gran cantidad de instancias en la memoria se reduzca significativamente:
Actualmente, este es el método principal para reducir sustancialmente la huella de memoria de una instancia de una clase en RAM.
Esta reducción se logra por el hecho de que en la memoria después del título del objeto, las referencias de objetos se almacenan: los valores de los atributos 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>})
Para automatizar el proceso de creación de una clase con __slots__
, hay una biblioteca [namedlist] ( https://pypi.org/project/namedlist ). La función namedlist.namedlist
crea una clase con __slots__
:
>>> Point = namedlist('Point', ('x', 'y', 'z'))
Otro paquete [attrs] ( https://pypi.org/project/attrs ) le permite automatizar el proceso de creación de clases con y sin __slots__
.
Tupla
Python también tiene una tuple
tipo incorporada para representar estructuras de datos inmutables. Una tupla es una estructura o registro fijo, pero sin nombres de campo. Para el acceso 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 de crear la instancia de tupla:
>>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR
Las instancias de tupla son bastante compactas:
>>> print(sys.getsizeof(ob)) 72
Ocupan 8 bytes en la memoria más que instancias de clases con __slots__
, ya que la traza de tuplas en la memoria también contiene varios campos:
Namedtuple
Dado que la tupla se usa ampliamente, un día hubo una solicitud de que aún podría tener acceso a los campos y también por su nombre. La respuesta a esta solicitud fue el módulo collections.namedtuple
.
La función namedtuple
está diseñada para automatizar el proceso de generación de tales clases:
>>> Point = namedtuple('Point', ('x', 'y', 'z'))
Crea una subclase de tupla, en la que se definen descriptores para acceder a los campos por nombre. Para nuestro ejemplo, se vería así:
class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(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 huella de memoria idéntica a la de una tupla. Un gran número de instancias deja una huella de memoria ligeramente mayor:
Recordclass: mutable namedtuple sin GC cíclico
Dado que la tuple
y, en consecuencia, las namedtuple
tuplas generan objetos inmutables en el sentido de que el atributo ob.x
ya no puede asociarse con otro objeto de valor, ha surgido una solicitud de una variante nombrada tupla mutable. Como no hay ningún tipo incorporado en Python que sea idéntico a la tupla que admite asignaciones, se han creado muchas opciones. Nos centraremos en [recordclass] ( https://pypi.org/project/recordclass ), que recibió una calificación de [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tupla-en- pitón / 29419745). Además, se puede usar para reducir el tamaño de los objetos en la RAM en comparación con el tamaño de los objetos tipo tuple
.
El paquete recordclass presenta el tipo recordclass.mutabletuple
, que es casi idéntico a la tupla, pero también admite asignaciones. Sobre esta base, se crean subclases que son casi completamente idénticas a las tuplas con nombre, pero que 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
, le permite automatizar la creación de estas clases:
>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)
Las instancias de clase tienen la misma estructura que las tuple
, pero solo sin PyGC_Head
:
Por defecto, la función recordclass
crea una clase que no participa en el mecanismo de recolección de basura cíclica. Típicamente, namedtuple
y recordclass
se usan para generar clases que representan registros o estructuras de datos simples (no recursivas). Usarlos correctamente en Python no genera referencias circulares. Por esta razón, a raíz de las instancias de clases generadas por recordclass
, de default, the
excluye el fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
structure, corresponding to the created class, in the
PyTypeObject structure, corresponding to the created class, in the
field, by default, the flag
banderas field, by default, the flag
Py_TPFLAGS_HAVE_GC` no está configurada).
El tamaño de la huella de memoria de una gran cantidad de instancias es menor que el de las instancias de la clase con __slots__
:
Objeto de datos
Otra solución propuesta en la biblioteca de clases de registros se basa en la idea: usar la misma estructura de almacenamiento en la memoria que en las instancias de clase con __slots__
, pero no participar en el mecanismo de recolección de basura cíclica. Dichas clases se generan utilizando la función recordclass.make_dataclass
:
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
La clase creada de esta manera, por defecto, crea instancias mutables.
Otra forma: use la declaración de clase con herencia de recordclass.dataobject
:
class Point(dataobject): x:int y:int z:int
Las clases creadas de esta manera crearán instancias que no participan en el mecanismo de recolección de basura cíclica. La estructura de la instancia en la memoria es la misma que en el caso de __slots__
, pero sin PyGC_Head
:
>>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40
Para acceder a los campos, también se utilizan descriptores especiales para acceder al campo por su desplazamiento desde el principio del objeto, que se encuentran 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 la huella de memoria de una gran cantidad de instancias es el mínimo posible para CPython:
Cython
Hay un enfoque basado en el uso de [Cython] ( https://cython.org ). Su ventaja es que los campos pueden tomar los valores de los tipos atómicos del 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
El rastreo de instancia en la memoria tiene la siguiente estructura:
El tamaño de la huella de una gran cantidad de copias es menor:
Sin embargo, debe recordarse que al acceder desde el código Python, se realizará una conversión de int
a un objeto Python y viceversa cada vez.
Numpy
El uso de matrices multidimensionales o matrices de registros para una gran cantidad 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 el uso de funciones del paquete numpy
.
>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])
Se crea una matriz de N
elementos, inicializados con ceros, utilizando la función:
>>> points = numpy.zeros(N, dtype=Point)
El tamaño de la matriz en la memoria es el mínimo posible:
El acceso normal a los elementos de la matriz y las filas requerirá la conversión de un objeto Python a un valor C int
y viceversa. La extracción de una sola fila da como resultado la creación de una matriz que contiene un único elemento. Su rastro ya no será tan compacto:
>>> 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
En un ejemplo claro y simple, fue posible verificar que la comunidad de desarrolladores y usuarios del lenguaje de programación Python (CPython) tiene posibilidades reales de una reducción significativa en la cantidad de memoria utilizada por los objetos.