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 eldict
, 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
otuple
, 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, comotyping.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]