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:
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:
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
):
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 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:
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:
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:
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
:
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__
:
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
:
>>> 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:
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 tamaño de rastreo de una gran cantidad de copias es menor:
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:
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.