¿En qué estructura se almacena un objeto Python en la memoria?

Digamos que tengo una clase A:

class A(object): def __init__(self, x): self.x = x def __str__(self): return self.x 

Y uso sys.getsizeof para ver cuántos bytes toma la instancia de A :

 >>> sys.getsizeof(A(1)) 64 >>> sys.getsizeof(A('a')) 64 >>> sys.getsizeof(A('aaa')) 64 

Como se ilustra en el experimento anterior, el tamaño de un objeto A es el mismo sin importar lo que sea self.x

Así que me pregunto cómo Python almacena un objeto internamente?

Depende de qué tipo de objeto, y también de qué implementación de Python 🙂

En CPython, que es lo que la mayoría de la gente usa cuando usa python , todos los objetos de Python están representados por una estructura C, PyObject . Todo lo que ‘almacena un objeto’ realmente almacena un PyObject * . La estructura PyObject contiene la información mínima: el tipo del objeto (un puntero a otro PyObject ) y su recuento de referencia (un entero de ssize_t ssize_t). Los tipos definidos en C extienden esta estructura con la información adicional que necesitan almacenar en el objeto en sí. y, a veces, asignar datos adicionales por separado.

Por ejemplo, las tuplas (implementadas como PyTupleObject “extendiendo” una estructura PyObject) almacenan su longitud y los punteros de PyObject que contienen dentro de la misma estructura (la estructura contiene una matriz de 1 longitud en la definición, pero la implementación asigna un bloque de memoria del tamaño correcto para mantener la estructura PyTupleObject más exactamente la cantidad de elementos que debe contener la tupla.) Del mismo modo, las cadenas ( PyStringObject ) almacenan su longitud, su hashvalue almacenado en caché, algunas cuentas de caché de cadenas (“interning”) y la Carácter real * de sus datos. Las tuplas y las cuerdas son, por lo tanto, bloques únicos de memoria.

Por otro lado, las listas ( PyListObject ) almacenan su longitud, un PyObject ** para sus datos y otro ssize_t para realizar un seguimiento de la cantidad de espacio que asignaron para los datos. Debido a que Python almacena los punteros de PyObject todas partes, no puede hacer crecer una estructura de PyObject una vez que se asigna; hacerlo puede requerir que la estructura se mueva, lo que significaría encontrar todos los punteros y actualizarlos. Debido a que una lista puede necesitar crecer, tiene que asignar los datos por separado de la estructura PyObject. Las tuplas y las cuerdas no pueden crecer, por lo que no necesitan esto. Los dictados ( PyDictObject ) funcionan de la misma manera, aunque almacenan la clave, el valor y el hashvalor en caché de la clave, en lugar de solo los elementos. Dict también tiene una sobrecarga adicional para acomodar pequeños dictados y funciones de búsqueda especializadas.

Pero estos son todos los tipos en C, y por lo general puede ver cuánta memoria usarían con solo mirar la fuente de C. Las instancias de clases definidas en Python en lugar de C no son tan fáciles. El caso más simple, las instancias de clases clásicas, no es tan difícil: es un PyObject que almacena un PyObject * en su clase (que no es lo mismo que el tipo almacenado en la estructura PyObject ya), un PyObject * en su atributo __dict__ (que contiene todos los demás atributos de la instancia) y un PyObject * a su lista débil (que es utilizada por el módulo weakref , y solo se inicializa si es necesario). El __dict__ la instancia suele ser exclusivo de la instancia, por lo que al calcular el “tamaño de memoria” de en tal caso, por lo general, también desea contar el tamaño del atributo dict. ¡Pero no tiene que ser específico a la instancia! __dict__ puede asignarse a muy bien.

Las clases de nuevo estilo complican los modales. A diferencia de las clases clásicas, las instancias de clases de nuevo estilo no son tipos C separados, por lo que no necesitan almacenar la clase del objeto por separado. Tienen espacio para la referencia __dict__ y debilidad en la lista, pero a diferencia de las instancias clásicas, no requieren el atributo __dict__ para los atributos arbitrarios. si la clase (y todas sus clases básicas) usan __slots__ para definir un conjunto estricto de atributos, y ninguno de esos atributos se llama __dict__ , la instancia no permite atributos arbitrarios y no se asigna ningún dict. Por otro lado, los atributos definidos por __slots__ deben almacenarse en algún lugar . Esto se hace almacenando los punteros de PyObject para los valores de esos atributos directamente en la estructura de PyObject, como ocurre con los tipos escritos en C. Cada entrada en __slots__ tomará un PyObject * , independientemente de si el atributo está establecido o no.

Dicho todo esto, el problema sigue siendo que dado que todo en Python es un objeto y todo lo que contiene un objeto solo contiene una referencia, a veces es muy difícil trazar la línea entre los objetos. Dos objetos pueden referirse al mismo bit de datos. Pueden tener las únicas dos referencias a esos datos. Deshacerse de ambos objetos también se deshace de los datos. ¿Ambos son dueños de los datos? Hace solo uno de ellos, pero si es así, ¿cuál? ¿O diría que poseen la mitad de los datos, aunque deshacerse de un objeto no libera la mitad de los datos? Las debilidades pueden hacer esto aún más complicado: dos objetos pueden referirse a los mismos datos, pero eliminar uno de los objetos puede hacer que el otro objeto también se libere de su referencia a esos datos, lo que hace que los datos se limpien después de todo.

Afortunadamente, el caso común es bastante fácil de entender. Hay depuradores de memoria para Python que hacen un trabajo razonable para hacer un seguimiento de estas cosas, como heapy . Y siempre que su clase (y sus clases básicas) sea razonablemente simple, puede hacer una conjetura educada sobre la cantidad de memoria que ocuparía, especialmente en grandes cantidades. Si realmente desea conocer los tamaños exactos de sus estructuras de datos, consulte la fuente de CPython; la mayoría de los tipos incorporados son estructuras simples descritas en Include/object.h e implementadas en Objects/object.c . La propia estructura de PyObject se describe en Include/object.h . Sólo tenga en cuenta: es punteros hacia abajo; los que también ocupan espacio.

en el caso de una nueva clase, getsizeof () devuelve el tamaño de una referencia a PyObject que es devuelto por la función C PyInstance_New ()

Si desea una lista de todos los tamaños de objeto, compruebe esto .