R

ReflexKR

Documentación de Reflex

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

El aeropuerto de las etiquetas: el modelo de objetos de Python
Lección 11

Tiempo 8:20–9:10

Maletas sin dueño: memoria y GC

Parte de equipaje perdido

LOST-011

Las maletas fantasma de la cinta 13

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.»

11-1. Contar etiquetas: el conteo de referencias (9 min)

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))
# 2
  • El primer valor es 2 y no 1 porque también cuenta la referencia que se pasa como argumento al llamar a sys.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
# back
  • Después de del 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».
  • Cuando una función termina, su casilla de llamada desaparece y el nombre local tmp se cae con ella. Por eso se retira antes de «back».
  • Esto es lo que decía la supervisora en la lección 6: aunque sean cosas de una función ya terminada, si la celda de una clausura las sujeta, el conteo de referencias no llega a 0 y se quedan.

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

En tags, cada vez que quitas una etiqueta las refs bajan 1, y la maleta se retira en la acción que las deja en 0. En la cuarta acción de chain, b no tiene etiquetas pero está dentro de a, así que todavía tiene 1 ref, y se retira junto con a.

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.

11-2. Dos maletas que se llevan la una a la otra: referencias circulares y GC (12 min)

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)

Después de la línea 9, las dos maletas se apuntan mutuamente con partner. Si después de la línea 10 desaparecen del tablero es porque el tablero solo dibuja las maletas a las que se llega partiendo de un nombre.

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).

  • Examina los objetos que pueden contener otros objetos, como listas, dicts o instancias.
  • Cuenta las referencias descontando las que se intercambian dentro de cada grupo. Un grupo que no recibe ninguna referencia de fuera es basura que solo se sujeta a sí misma.
  • Retira esos grupos enteros. 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
# True
  • Después de del a, b no se retira nada. «discard» solo se imprime dentro de gc.collect(). El orden de esas dos líneas no está fijado.
  • Normalmente no hace falta llamar a gc.collect(). Cuando se crean muchos objetos, el intérprete lo ejecuta por su cuenta. gc.disable() solo se ha usado para el ejemplo.
  • Cuándo y en cuántas fases se ejecuta el GC automático cambia de una versión a otra, e incluso ha cambiado dentro de 3.14. No escribas código que dependa de un momento o de un número concretos.

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.: 23

El 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

En cycle, aunque quites todas las etiquetas, a cada maleta le queda 1 ref, y gc.collect() retira 4 objetos: las 2 maletas y las 2 listas que llevan dentro. break rompe primero el ciclo y se retira sin gc (gc.collect() da 0), y self es el [[...]] de la lección 3.

Como los ciclos acaban retirándose, ¿no pasa nada? Hay dos diferencias.

  • El conteo de referencias retira el objeto en la misma línea en que llega a 0, pero el GC de ciclos lo limpia todo más tarde, de golpe. Mientras tanto, siguen ocupando memoria.
  • Si un objeto dentro de un ciclo tiene abierto un archivo o una conexión, eso tampoco se cierra hasta que se ejecuta el GC.

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.

11-3. Etiquetas punteadas: referencias débiles (8 min)

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())
# None
  • Para obtener la maleta hay que llamarla, como en w(). Si ya se ha retirado, sale None, así que compruébalo siempre antes de usarla.
  • No a todos los objetos se les puede poner una etiqueta punteada. 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

La etiqueta punteada w no entra en las refs. En weak, la maleta se retira en cuanto quitas la última etiqueta; en weak+cycle, aunque haya una etiqueta punteada, el ciclo lo retira gc.collect().

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 0

Tambié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)
# False

finalize 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.

11-4. No te fíes de __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.

  1. No se sabe cuándo se llamará. Si el objeto está en un ciclo, se retrasa hasta que se ejecute el GC. En otras implementaciones de Python que no usan conteo de referencias, como PyPy, puede tardar todavía más.
  2. Las excepciones que se producen dentro no salen hacia fuera. Se imprimen una vez en la salida de error estándar y se ignoran.
  3. No hay garantía de que se llame a __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 running

Los 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 stuck

Los 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.

11-5. Objetos inmortales, el peso de una maleta y 3.14t (5 min)

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 False
  • El número es un valor marcador que significa «inmortal», así que cambia según la versión. En CPython 3.14 es 3221225472; en 3.13 era 4294967295.
  • sys._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 64

En 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().

Caso cerrado (10 min)

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 None

Con 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.

Ejercicios

  1. Predice el 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?
  2. Mete una cadena como valor en un weakref.WeakValueDictionary(). ¿Qué error sale? ¿Y si la envuelves en una instancia de una clase propia?
  3. Crea una clase que abra un archivo y lo cierre en __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.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

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.

Se muestra como texto plano, sin enlaces ni formato.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex