Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿cached_property se ejecuta dos veces en Python 3.12? Añade single-flight solo si hace falta

Python 3.12 quitó el bloqueo antiguo de cached_property. Demuestra primero que la duplicación importa y guarda el resultado correcto bajo un bloqueo por instancia.

Árbol para elegir cached_property normal o single-flight por instancia según concurrencia e idempotencia.
Ilustración explicativa original: sincroniza solo cuando el trabajo duplicado afecta a la corrección o tiene un coste inaceptable. Watch at Ease

Que la caché quede coherente no significa que el getter se ejecute una sola vez. Desde Python 3.12, dos hilos pueden entrar en la primera lectura de la misma instancia antes de que cualquiera guarde el valor.

No añadas un bloqueo por reflejo. Un cálculo puro e idempotente sobre datos inmutables suele duplicar solo el coste. Si cobra, crea archivos, asigna identificadores externos, cambia estado compartido o depende de identidad, trata la ejecución única como requisito independiente.

1. Demuestra la carrera antes de cambiar el diseño

Sincroniza dos hilos con una barrera para la primera lectura de la misma instancia. Incrementa un contador al entrar y espera brevemente. En Python 3.12 o posterior, dos ejecuciones están permitidas aunque ambos lectores terminen con el mismo valor.

Anota la versión, que comparten instancia y si el marco sustituye cached_property. Crear dos instancias demuestra otro problema.

2. Clasifica por consecuencia, no solo por coste

Un cálculo puro sobre campos inmutables suele ser idempotente; repetirlo desperdicia tiempo. Unificar trabajo caro es una decisión de rendimiento.

Inserciones, pagos, archivos, solicitudes, contadores y registros afectan a la corrección. Usa también claves idempotentes o transacciones cuando sea posible; un bloqueo local no garantiza exactamente una vez entre procesos.

3. Guarda el valor bajo el bloqueo de la instancia

Usa property normal, un centinela privado, un campo de caché y un bloqueo por objeto. La ruta rápida devuelve; la lenta bloquea, vuelve a comprobar, calcula, asigna y libera.

Bloquear solo dentro del getter de cached_property y dejar que el decorador escriba después de liberar puede mantener una pequeña ventana de carrera.

4. Conserva la semántica de fallo e invalidación

Asigna solo tras terminar con éxito. Una excepción deja el centinela para que el siguiente acceso reintente. No publiques un objeto mutable a medio construir.

Invalida bajo el mismo bloqueo y decide si lectores en curso pueden usar el valor anterior. No uses None como centinela si es un resultado válido.

5. No recrees la contención de Python 3.11

Un bloqueo en el descriptor o la clase serializa instancias independientes y repite el problema eliminado. Crear un bloqueo nuevo en cada acceso tampoco protege nada.

Si un bloqueo por objeto cuesta demasiado, considera construcción anticipada, precomputación inmutable, una caché que fusione solicitudes o rediseñar el efecto lateral.

6. Acepta el arreglo con cuatro pruebas

Comprueba una ejecución con dos lecturas concurrentes de una instancia; solapamiento entre dos instancias; reintento tras el primer fallo; y un solo valor nuevo después de invalidar bajo el bloqueo.

Ejecuta en todas las versiones compatibles. Verifica el contrato de tu aplicación, no una implementación histórica no documentada.

Flujo por instancia: revisar centinela, bloquear, revisar de nuevo, calcular y asignar antes de liberar.
Ilustración explicativa original: la segunda revisión y la asignación bajo bloqueo cierran la carrera. Watch at Ease