El aeropuerto de las etiquetas: el modelo de objetos de Python
·
Resumen del curso
1
La etiqueta no es la maleta: nombres y objetos
2
Maletas selladas: mutable e inmutable
3
Maletas dentro de maletas: contenedores y copias
4
Al entregar una maleta: funciones y argumentos
5
Zonas y pases: ámbitos
6
Equipaje de mano: clausuras
7
La especificación de la maleta: clases e instancias
8
Procedimientos estándar del aeropuerto: métodos dunder
9
La especificación de la especificación: herencia y MRO
10
El control: el secreto del acceso a atributos
11
Maletas sin dueño: memoria y GC
12
Números de taquilla: hash, igualdad y repaso final
Tiempo 8:20–9:10
Parte de equipaje perdido
LOST-011
Lo reporta
Yoon, del turno de noche de revisión de instalaciones
Qué se perdió
Dos maletas sin ninguna etiqueta que no se retiran. Llevan once días dando vueltas en la cinta 13.
Para cerrar el caso
Desvelar por qué no se retiran las dos maletas, y hacer que, una vez retiradas, no vuelvan a aparecer.
Aeropuerto Internacional Haneulseom · Centro de equipajes
Las dos maletas de la cinta 13, de las que llegan avisos desde el primer día, siguen dando vueltas hoy. No tienen ni una etiqueta. Según las normas, una maleta sin etiquetas tiene que ir ese mismo día a la zona de desechos.
La supervisora Do Miran extiende sobre la mesa tres registros anteriores: la radiografía de la lección 3, la nota de la lección 6 y el registro de partner de la lección 7. «Hoy vamos a averiguar qué son esas dos. Pero antes hay que entender bien la regla de retirada.»
En CPython, cada objeto lleva un número que cuenta cuántas referencias apuntan a él. Es el conteo de referencias (reference count). Al colgar una etiqueta sube 1, al quitarla baja 1, y en el momento en que llega a 0 el objeto se retira en el acto.
import sys
a = []
print(sys.getrefcount(a))
# 2
b = a
print(sys.getrefcount(a))
# 3
c = [a, a]
print(sys.getrefcount(a))
# 5
del b, c
print(sys.getrefcount(a))
# 2sys.getrefcount(a).c = [a, a] mete dos etiquetas dentro de la lista, así que suma 2. Las etiquetas metidas dentro de una maleta también son etiquetas.del b, c quita el nombre b y, al retirarse la lista c, desaparecen también las dos etiquetas que llevaba dentro.No te fíes del número en sí. Desde 3.14, el intérprete aplica una optimización que se salta algunos incrementos y decrementos de referencias. Si ejecutas el mismo código dentro de una función, el primer valor es 1 en CPython 3.14 y 2 en 3.13. Lo que hay que mirar es la diferencia: «1 por etiqueta».
Que se retira en el momento de llegar a 0 se puede comprobar con __del__, el método dunder que se llama justo antes de retirar un objeto (por qué no hay que fiarse de él, en 11-4).
class Bag:
def __init__(self, label):
self.label = label
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = a
del a
print("after del a")
del b
print("after del b")
# after del a
# discard A
# after del b
def check_in():
tmp = Bag("T")
print("inside")
check_in()
print("back")
# inside
# discard T
# backdel a todavía queda la etiqueta b, así que no se retira. En el del b, que quita la última etiqueta, se imprime al momento «discard A».tmp se cae con ella. Por eso se retira antes de «back».El Observatorio de GC de abajo crea objetos de verdad en un proceso trabajador y ejecuta el gc de verdad. Cada maleta muestra sus «refs» (etiquetas + etiquetas metidas en otras maletas). Las maletas a las que no se llega desde ninguna etiqueta, ni siguiendo lo que hay dentro de otras maletas, se dibujan con línea discontinua roja. Las etiquetas punteadas muestran con w() → … a qué apuntan ahora.
Observatorio de GC
Python
Pin dice
→ El número de sys.getrefcount cambia según la versión y el sitio (en el módulo o dentro de una función). No mires el valor absoluto, sino la «diferencia»: sube y baja 1 cada vez que cuelgas o quitas una etiqueta.
Si juntamos los tres registros, aparece la respuesta. El [[...]] de la radiografía de la lección 3 era una maleta que se llevaba a sí misma dentro. En el registro de la lección 7, las dos maletas se apuntaban mutuamente con el atributo partner. Pasado a código, queda así:
Tablero de etiquetas
Paso 1 / 0
Lo siguiente que se ejecuta: 0
Preparando el laboratorio…
Acaba de cambiar
Maleta sellada (inmutable)
Con del a, b han caído todas las etiquetas con nombre. Pero dentro de A queda una etiqueta que apunta a B, y dentro de B, una que apunta a A. Las dos tienen un conteo de referencias de 1, que nunca llegará a 0. Esto es una referencia circular (reference cycle).
Un objeto al que no se puede llegar partiendo de un nombre es basura: el programa no tiene forma de volver a usarlo. Por eso desapareció del tablero. Pero el conteo de referencias por sí solo no puede retirar esta basura.
Por eso CPython tiene un segundo equipo de limpieza: el recolector de basura de ciclos (el módulo gc).
gc.collect() le encarga ese trabajo en el acto y devuelve el número de objetos retirados.import gc
gc.disable() # desactiva un momento el GC automático para que el ejemplo sea estable
class Bag:
def __init__(self, label):
self.label = label
self.partner = None
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
del a, b
print("tags gone")
print(gc.collect() > 0)
gc.enable()
# tags gone
# discard A
# discard B
# Truedel a, b no se retira nada. «discard» solo se imprime dentro de gc.collect(). El orden de esas dos líneas no está fijado.gc.collect(). Cuando se crean muchos objetos, el intérprete lo ejecuta por su cuenta. gc.disable() solo se ha usado para el ejemplo.La lista que se contenía a sí misma de la lección 3 es el mismo caso.
import gc
a = []
a.append(a)
print(a, a[0] is a)
# [[...]] True
del a
print(gc.collect())
# p. ej.: 23El número devuelto cuenta también la otra basura acumulada hasta ese momento, así que depende de la situación y de la versión. En este PC, el mismo archivo dio 1 con 3.13.9 y 23 con 3.14.2. El observatorio de abajo hace una limpieza al empezar y cuenta a partir de ahí, así que siempre da el mismo valor.
Observatorio de GC
Python
Como los ciclos acaban retirándose, ¿no pasa nada? Hay dos diferencias.
Hay dos formas de eliminar un ciclo: quitar a mano una de las etiquetas cuando ya no hacen falta (como en break), o hacer desde el principio que uno de los lados sea una referencia débil.
Una referencia débil (weak reference) es una etiqueta punteada que no entra en el conteo de referencias. Apunta a la maleta, pero no la sujeta. Cuando la maleta se retira, la etiqueta punteada se vacía sola.
import weakref
class Bag:
def __init__(self, label):
self.label = label
a = Bag("A")
w = weakref.ref(a)
print(w() is a)
# True
del a
print(w())
# Nonew(). Si ya se ha retirado, sale None, así que compruébalo siempre antes de usarla.weakref.ref([1, 2]) da TypeError: cannot create weak reference to 'list' object. Lo mismo pasa con int, str, tuple y dict; con las instancias de clases propias, sí se puede.Observatorio de GC
Python
Donde más se usan las referencias débiles es en las cachés. Si haces una caché con un dict normal, la caché sujeta para siempre los objetos que guardas y la memoria no baja. WeakValueDictionary guarda los valores con etiquetas punteadas, así que, cuando ya no se usan en ningún otro sitio, las entradas desaparecen solas.
import weakref
class Bag:
def __init__(self, label):
self.label = label
desk = weakref.WeakValueDictionary()
c = Bag("C")
desk["C"] = c
print("C" in desk, len(desk))
# True 1
del c
print("C" in desk, len(desk))
# False 0También existen WeakKeyDictionary, que guarda las claves con etiquetas punteadas, y WeakSet, que guarda así sus elementos. Sirven para añadir información aparte a un objeto sin alargar su vida.
Para dejar programado algo que hacer cuando se retire una maleta, usa weakref.finalize.
import weakref
class Bag:
def __init__(self, label):
self.label = label
d = Bag("D")
f = weakref.finalize(d, print, "discard D")
print(f.alive)
# True
del d
# discard D
print(f.alive)
# Falsefinalize se llama como mucho una vez, y si la maleta sigue viva cuando termina el programa, se llama al salir. Es un mecanismo más fiable que el __del__ del siguiente apartado. Eso sí, si la función o los argumentos que le pasas sujetan esa maleta (por ejemplo, pasando d.close), la maleta no se retirará nunca, así que ten cuidado.
__del__: la limpieza, con with (6 min)__del__ parece un buen sitio para el código de limpieza, pero no es fiable por tres motivos.
__del__ en los objetos que siguen vivos al terminar el programa.class Jam:
def __del__(self):
raise RuntimeError("belt jam")
x = Jam()
del x
print("still running")
# (error estándar) p. ej.: Exception ignored while calling deallocator <function Jam.__del__ at 0x...>:
# (error estándar) ... RuntimeError: belt jam
# still runningLos recursos que hay que cerrar sí o sí, como archivos, sockets o cerrojos, se cierran con with. En el momento de salir del bloque, aunque salte una excepción, se llama a __exit__ (lección 8).
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, *exc):
print("belt off")
return False
with Belt() as belt:
print("moving")
# belt on
# moving
# belt off
try:
with Belt():
raise ValueError("stuck")
except ValueError as e:
print("caught", e)
# belt on
# belt off
# caught stuckLos archivos que devuelve open() también se usan con with open(path) as f:. Así, el momento de la limpieza queda fijado en un punto visible del código.
Pin dice
→ __del__ no promete «cuándo». Lo que haya que cerrar sí o sí, con with; la limpieza que deba ir ligada a la vida del objeto, con weakref.finalize.
Los objetos que se usan en todas partes, como None, True o los enteros pequeños, son objetos inmortales (3.12, PEP 683). Su conteo de referencias está fijo, así que, por muchas etiquetas que cuelgues y quites, nunca se retiran.
import sys
print(sys.getrefcount(None))
# 3221225472
x = None
print(sys.getrefcount(None))
# 3221225472
print(sys._is_immortal(None), sys._is_immortal([]))
# True Falsesys._is_immortal apareció en 3.14. Como su nombre empieza por guion bajo, es un detalle de implementación de CPython.sys.getsizeof solo pesa la propia maleta. No cuenta el contenido.
import sys
print(sys.getsizeof([]), sys.getsizeof([1, 2, 3]))
# 56 88
big = [0] * 1000
print(sys.getsizeof(big), sys.getsizeof([big]))
# 8056 64En CPython de 64 bits, una lista ocupa 56 bytes de cuerpo vacío más 8 bytes por cada hueco donde meter una etiqueta. Según cómo se cree, a veces reserva huecos de sobra, por eso [1, 2, 3] ocupa 88 y no 80. [big] es una maleta de un solo hueco, así que ocupa 64 bytes. Aunque el big de dentro ocupe 8056 bytes, no se cuenta.
Unas palabras sobre la compilación free-threaded (3.14t). En la compilación sin GIL, varios hilos modifican a la vez los conteos de referencias. Por eso, el hilo que creó el objeto cuenta rápido su propia parte, y los demás hilos cuentan aparte, de forma atómica, una parte compartida (biased reference counting). Las reglas que se ven desde fuera son las mismas. Al ejecutar los ejemplos de esta lección con 3.14.3t (con sys._is_gil_enabled() en False), coincidieron los valores del conteo de referencias, la retirada inmediata y la limpieza de ciclos con gc.collect(). Lo único distinto fueron los números que devolvía gc.collect().
Las dos maletas eran una referencia circular: cada una sujetaba a la otra con su partner. No tenían etiquetas, pero a cada una le quedaba 1 en el conteo de referencias, así que no se retiraban, y se quedaron en la cinta hasta que se ejecutara el GC de ciclos. La supervisora ejecuta gc.collect() y la cinta 13 se vacía.
Para que no vuelva a pasar, hacemos que cada maleta recuerde a su pareja con una etiqueta punteada. Envolviéndolo en la property de la lección 10, el código que la usa no cambia.
import weakref
class Bag:
def __init__(self, label):
self.label = label
self._partner = None
@property
def partner(self):
return self._partner() if self._partner is not None else None
@partner.setter
def partner(self, other):
self._partner = weakref.ref(other) # recuerda a la pareja con una etiqueta punteada
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
print(a.partner.label, b.partner.label)
# B A
watch = weakref.ref(a)
del a
print(watch() is None, b.partner)
# True NoneCon una sola línea, del a, A se retira al momento y el partner de B pasa a ser None. Desde el principio no hay ningún ciclo que tenga que esperar al GC.
Antes de terminar la jornada, Yoon trae una línea más del registro. La maleta fantasma B estuvo guardada el mes pasado en la taquilla de objetos perdidos, y entonces nadie consiguió sacarla. La supervisora deja la llave de la taquilla sobre la mesa: «Eso lo vemos mañana, como último caso.»
Informe de cierre — LOST-011. Las dos maletas fantasma eran una referencia circular: se sujetaban mutuamente con partner. Retiradas con gc.collect(); para evitar que se repita, ahora recuerdan a su pareja con weakref.ref.
sys.getrefcount(a) después de a = [] y a.append(a). ¿Qué sale si lo ejecutas en el módulo, y por qué no es 2?weakref.WeakValueDictionary(). ¿Qué error sale? ¿Y si la envuelves en una instancia de una clase propia?__del__, y luego cámbiala para que lo cierre con __enter__ y __exit__. Comprueba que se cierra aunque salte una excepción dentro del bloque.La libreta de la supervisora
Conteo de referencias = número de etiquetas que lleva la maleta. Se retira en la misma línea en que llega a 0.
sys.getrefcount también cuenta la referencia pasada como argumento. El valor cambia según la versión: mira solo la diferencia.
Las maletas que se llevan la una a la otra (referencia circular) no llegan a 0. El GC de ciclos las retira más tarde. gc.collect() devuelve cuántas retiró.
El momento del GC automático y su organización en generaciones cambian según la versión. No dependas de ello.
Una referencia débil es una etiqueta punteada. Para cachés, WeakValueDictionary; para lo que hay que hacer al retirarse, weakref.finalize.
No te fíes de __del__. Los recursos que hay que cerrar, con with.
Los objetos inmortales como None no se retiran. getsizeof solo pesa el cuerpo de la maleta.
← Anterior
Lección 10 · El control: el secreto del acceso a atributos
Siguiente →
Lección 12 · Números de taquilla: hash, igualdad y repaso final
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
R
ReflexKR
Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.
ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café
Aviso de afiliados© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Preguntas y respuestas
Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.