python class memory-management instance cpython

python - ¿Qué recursos utiliza una instancia de una clase?



class memory-management (4)

¿Qué tan eficiente es python (cpython supongo) cuando se asignan recursos para una instancia de una clase recién creada? Tengo una situación en la que necesitaré crear una instancia de una clase de nodo millones de veces para hacer una estructura de árbol. Cada uno de los objetos de nodo debe ser liviano, solo debe contener algunos números y referencias a los nodos padre e hijo.

Por ejemplo, Python necesitará asignar memoria para todas las propiedades de "doble guión bajo" de cada objeto instanciado (por ejemplo, las cadenas de documentación, __dict__ , __repr__ , __class__ , etc, etc.), ya sea para crear estas propiedades individualmente o para almacenar los punteros donde están definido por la clase? ¿O es eficiente y no necesita almacenar nada, excepto las cosas personalizadas que definí que deben almacenarse en cada objeto?


¿Es eficiente y no necesita almacenar nada, excepto las cosas personalizadas que definí que deben almacenarse en cada objeto?

Casi sí, salvo cierto espacio. La clase en Python ya es una instancia de type , llamada metaclase. Cuando se __init__ una nueva instancia de clase objeto, las custom stuff son solo esas cosas en __init__ . Los atributos y métodos definidos en clase no gastarán más espacio.

En cuanto a cierto espacio, solo refiera la respuesta de Reblochon Masque, muy buena e impresionante.

Tal vez pueda dar un ejemplo simple pero ilustrativo:

class T(object): def a(self): print(self) t = T() t.a() # output: <__main__.T object at 0x1060712e8> T.a(t) # output: <__main__.T object at 0x1060712e8> # as you see, t.a() equals T.a(t) import sys sys.getsizeof(T) # output: 1056 sys.getsizeof(T()) # output: 56


El objeto más básico en CPython es solo una referencia de tipo y un recuento de referencias . Ambos tienen el tamaño de una palabra (es decir, 8 bytes en una máquina de 64 bits), por lo que el tamaño mínimo de una instancia es de 2 palabras (es decir, 16 bytes en una máquina de 64 bits).

>>> import sys >>> >>> class Minimal: ... __slots__ = () # do not allow dynamic fields ... >>> minimal = Minimal() >>> sys.getsizeof(minimal) 16

Cada instancia necesita espacio para __class__ y un recuento de referencias ocultas.

La referencia de tipo (aproximadamente el object.__class__ ) significa que las instancias obtienen contenido de su clase . Todo lo que defina en la clase, no la instancia, no ocupa espacio por instancia.

>>> class EmptyInstance: ... __slots__ = () # do not allow dynamic fields ... foo = ''bar'' ... def hello(self): ... return "Hello World" ... >>> empty_instance = EmptyInstance() >>> sys.getsizeof(empty_instance) # instance size is unchanged 16 >>> empty_instance.foo # instance has access to class attributes ''bar'' >>> empty_instance.hello() # methods are class attributes! ''Hello World''

Tenga en cuenta que los métodos también son funciones en la clase . La obtención de uno a través de una instancia invoca el protocolo del descriptor de datos de la función para crear un objeto de método temporal vinculando parcialmente la instancia a la función. Como resultado, los métodos no aumentan el tamaño de la instancia .

Las instancias no necesitan espacio para los atributos de clase, incluidos __doc__ y cualquier método.

Lo único que aumenta el tamaño de las instancias es el contenido almacenado en la instancia. Hay tres formas de lograr esto: __dict__ , __slots__ y tipos de contenedor . Todos estos almacenan el contenido asignado a la instancia de alguna manera.

  • De forma predeterminada, las instancias tienen un campo __dict__ , una referencia a una asignación que almacena atributos. Estas clases también tienen algunos otros campos predeterminados, como __weakref__ .

    >>> class Dict: ... # class scope ... def __init__(self): ... # instance scope - access via self ... self.bar = 2 # assign to instance ... >>> dict_instance = Dict() >>> dict_instance.foo = 1 # assign to instance >>> sys.getsizeof(dict_instance) # larger due to more references 56 >>> sys.getsizeof(dict_instance.__dict__) # __dict__ takes up space as well! 240 >>> dict_instance.__dict__ # __dict__ stores attribute names and values {''bar'': 2, ''foo'': 1}

    Cada instancia que usa __dict__ usa espacio para el dict , los nombres de atributos y los valores.

  • Agregar un campo __slots__ a la clase genera instancias con un diseño de datos fijo. Esto restringe los atributos permitidos a los declarados, pero ocupa poco espacio en la instancia. Las ranuras __dict__ y __weakref__ solo se crean a petición.

    >>> class Slots: ... __slots__ = (''foo'',) # request accessors for instance data ... def __init__(self): ... # instance scope - access via self ... self.foo = 2 ... >>> slots_instance = Slots() >>> sys.getsizeof(slots_instance) # 40 + 8 * fields 48 >>> slots_instance.bar = 1 AttributeError: ''Slots'' object has no attribute ''bar'' >>> del slots_instance.foo >>> sys.getsizeof(slots_instance) # size is fixed 48 >>> Slots.foo # attribute interface is descriptor on class <member ''foo'' of ''Slots'' objects>

    Cada instancia que usa __slots__ usa espacio solo para los valores de atributo.

  • La herencia de un tipo de contenedor, como list , dict o tuple , permite almacenar elementos ( self[0] ) en lugar de atributos ( self.a ). Esto utiliza un almacenamiento interno compacto además de __dict__ o __slots__ . Estas clases rara vez se construyen de forma manual; a menudo se utilizan ayudantes, como typing.NamedTuple .

    >>> from typing import NamedTuple >>> >>> class Named(NamedTuple): ... foo: int ... >>> named_instance = Named(2) >>> sys.getsizeof(named_instance) 56 >>> named_instance.bar = 1 AttributeError: ''Named'' object has no attribute ''bar'' >>> del named_instance.foo # behaviour inherited from container AttributeError: can''t delete attribute >>> Named.foo # attribute interface is descriptor on class <property at 0x10bba3228> >>> Named.__len__ # container interface/metadata such as length exists <slot wrapper ''__len__'' of ''tuple'' objects>

    Cada instancia de un contenedor derivado se comporta como el tipo base, más el potencial de __slots__ o __dict__ .

Las instancias más ligeras usan __slots__ para almacenar solo valores de atributos.

Tenga en cuenta que una parte de la sobrecarga de __dict__ se suele optimizar con los intérpretes de Python. CPython es capaz de compartir claves entre instancias , lo que puede reducir considerablemente el tamaño por instancia . PyPy utiliza una representación de clave compartida optimizada que elimina completamente la diferencia entre __dict__ y __slots__ .

No es posible medir con precisión el consumo de memoria de los objetos en todos los casos, excepto en los más triviales. La medición del tamaño de los objetos aislados pierde estructuras relacionadas, como __dict__ usando memoria para un puntero en la instancia y un dict externo. La medición de grupos de objetos con errores compartidos objetos (cadenas internadas, enteros pequeños, ...) y objetos perezosos (por ejemplo, el dict de __dict__ solo existe cuando se accede). Tenga en cuenta que PyPy no implementa sys.getsizeof para evitar su mal uso .

Para medir el consumo de memoria, se debe utilizar un programa completo de medición. Por ejemplo, uno puede usar resource o psutils para obtener el propio consumo de memoria mientras genera objetos .

He creado uno de estos scripts de medición para la cantidad de campos , la cantidad de instancias y la variante de implementación . Los valores que se muestran son bytes / campo para un recuento de instancias de 1000000, en CPython 3.7.0 y PyPy3 3.6.1 / 7.1.1-beta0.

# fields | 1 | 4 | 8 | 16 | 32 | 64 | ---------------+-------+-------+-------+-------+-------+-------+ python3: slots | 48.8 | 18.3 | 13.5 | 10.7 | 9.8 | 8.8 | python3: dict | 170.6 | 42.7 | 26.5 | 18.8 | 14.7 | 13.0 | pypy3: slots | 79.0 | 31.8 | 30.1 | 25.9 | 25.6 | 24.1 | pypy3: dict | 79.2 | 31.9 | 29.9 | 27.2 | 24.9 | 25.0 |

Para CPython, __slots__ guarda alrededor del 30% -50% de la memoria en comparación con __dict__ . Para PyPy, el consumo es comparable. Curiosamente, PyPy es peor que CPython con __slots__ , y se mantiene estable para conteos de campo extremos.


Superficialmente es bastante simple: los métodos, las variables de clase y la cadena de documentación de la clase se almacenan en la clase (las cadenas de documentación de la función se almacenan en la función). Las variables de instancia se almacenan en la instancia. La instancia también hace referencia a la clase para que pueda buscar los métodos. Normalmente, todos ellos se almacenan en diccionarios (el __dict__ ).

Entonces, sí, la respuesta corta es: Python no almacena métodos en las instancias, pero todas las instancias deben tener una referencia a la clase.

Por ejemplo, si tienes una clase simple como esta:

class MyClass: def __init__(self): self.a = 1 self.b = 2 def __repr__(self): return f"{self.__class__.__name__}({self.a}, {self.b})" instance_1 = MyClass() instance_2 = MyClass()

Luego en memoria se ve (muy simplificado) así:

Yendo mas profundo

Sin embargo, hay algunas cosas que son importantes al profundizar en CPython:

  • Tener un diccionario como abstracción conlleva bastante sobrecarga: necesita una referencia al diccionario de instancia (bytes) y cada entrada en el diccionario almacena el hash (8 bytes), un puntero a una clave (8 bytes) y un puntero a la Atributo almacenado (otros 8 bytes). También los diccionarios generalmente se asignan en exceso para que agregar otro atributo no active el cambio de tamaño del diccionario.
  • Python no tiene "tipos de valor", incluso un entero será una instancia. Eso significa que no necesita 4 bytes para almacenar un número entero. Python necesita (en mi computadora) 24 bytes para almacenar el número entero 0 y al menos 28 bytes para almacenar números enteros diferentes de cero. Sin embargo, las referencias a otros objetos solo requieren 8 bytes (puntero).
  • CPython utiliza el recuento de referencias, por lo que cada instancia necesita un recuento de referencia (8 bytes). Además, la mayoría de las clases de CPythons participan en el recolector de basura cíclico, que genera una sobrecarga de otros 24 bytes por instancia. Además de estas clases que pueden ser de referencia débil (la mayoría de ellas) también tienen un campo __weakref__ (otros 8 bytes).

En este punto, también es necesario señalar que CPython optimiza para algunos de estos "problemas":

  • Python utiliza diccionarios de uso compartido de claves para evitar algunos de los gastos generales de memoria (hash y clave) de los diccionarios de instancia.
  • Puedes usar __slots__ en las clases para evitar __dict__ y __weakref__ . Esto puede dar una huella de memoria significativamente menor por instancia.
  • Python interna algunos valores, por ejemplo, si creas un entero pequeño, no creará una nueva instancia de entero sino que devolverá una referencia a una instancia ya existente.

Dado todo eso y que varios de estos puntos (especialmente los puntos sobre la optimización) son detalles de implementación, es difícil dar una respuesta canónica sobre los requisitos de memoria efectivos de las clases de Python.

Reduciendo la huella de memoria de las instancias.

Sin embargo, en caso de que desee reducir la huella de memoria de sus instancias, definitivamente __slots__ . Tienen desventajas, pero en caso de que no se apliquen a usted, son una muy buena manera de reducir la memoria.

class Slotted: __slots__ = (''a'', ''b'') def __init__(self): self.a = 1 self.b = 1

Si eso no es suficiente y opera con muchos "tipos de valor", también podría ir un paso más allá y crear clases de extensión. Estas son clases que están definidas en C pero están envueltas para que puedas usarlas en Python.

Para mayor comodidad, estoy usando los enlaces de IPython para Cython aquí para simular una clase de extensión:

%load_ext cython

%%cython cdef class Extensioned: cdef long long a cdef long long b def __init__(self): self.a = 1 self.b = 1

Midiendo el uso de la memoria

La pregunta interesante que queda después de toda esta teoría es: ¿Cómo podemos medir la memoria?

También uso una clase normal:

class Dicted: def __init__(self): self.a = 1 self.b = 1

Generalmente estoy usando psutil (aunque es un método proxy) para medir el impacto de la memoria y simplemente mido la cantidad de memoria que usó antes y después. Las medidas son un poco de compensación porque necesito mantener las instancias en la memoria de alguna manera, de lo contrario la memoria se reclamaría (inmediatamente). Además, esto es solo una aproximación porque Python en realidad hace un poco de mantenimiento de la memoria, especialmente cuando hay un montón de crear / eliminar.

import os import psutil process = psutil.Process(os.getpid()) runs = 10 instances = 100_000 memory_dicted = [0] * runs memory_slotted = [0] * runs memory_extensioned = [0] * runs for run_index in range(runs): for store, cls in [(memory_dicted, Dicted), (memory_slotted, Slotted), (memory_extensioned, Extensioned)]: before = process.memory_info().rss l = [cls() for _ in range(instances)] store[run_index] = process.memory_info().rss - before l.clear() # reclaim memory for instances immediately

La memoria no será exactamente idéntica para cada ejecución porque Python reutiliza algo de memoria y, a veces, también mantiene la memoria para otros propósitos, pero al menos debería dar una pista razonable:

>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2 (15.625, 5.3359375, 2.7265625)

Utilicé el min aquí principalmente porque me interesaba cuál era el mínimo y lo dividí por 1024**2 para convertir los bytes a MegaBytes.

Resumen: Como era de esperar, la clase normal con dict necesitará más memoria que las clases con ranuras, pero las clases de extensión (si corresponde y están disponibles) pueden tener una huella de memoria aún menor.

Otra herramienta que podría ser muy útil para medir el uso de la memoria es memory_profiler , aunque no lo he usado en mucho tiempo.


[editar] No es fácil obtener una medición precisa del uso de la memoria mediante un proceso de Python; No creo que mi respuesta responda completamente a la pregunta , pero es un enfoque que puede ser útil en algunos casos.

La mayoría de los enfoques utilizan métodos proxy (crear n objetos y estimar el impacto en la memoria del sistema), y bibliotecas externas que intentan envolver esos métodos. Por ejemplo, los hilos se pueden encontrar here , here y there [/ editar]

En cPython 3.7 , el tamaño mínimo de una instancia de clase regular es de 56 bytes; con __slots__ (sin diccionario), 16 bytes.

import sys class A: pass class B: __slots__ = () pass a = A() b = B() sys.getsizeof(a), sys.getsizeof(b)

salida:

56, 16

Las cadenas de documentación, las variables de clase y las anotaciones de tipo no se encuentran en el nivel de instancia:

import sys class A: """regular class""" a: int = 12 class B: """slotted class""" b: int = 12 __slots__ = () a = A() b = B() sys.getsizeof(a), sys.getsizeof(b)

salida:

56, 16

[editar] Además, vea la respuesta de @LiuXiMin para una medida del tamaño de la definición de clase . [/editar]