El ingrediente secreto detrás de un código limpio.
¿Sabes cómo la documentación está llena de funciones que lees por encima? ¿Esas que parecen específicas pero que solucionan silenciosamente clases enteras de errores? Esta es esa lista. Breve, práctica y, lo más importante, poco usada. Cada concepto que encontrarás a continuación ha sido probado en sistemas reales (léase: me ha evitado interrupciones vergonzosas del servicio) y cada uno incluye un pequeño ejemplo que puedes incorporar a un proyecto ahora mismo.
Sin rodeos. Sin repetir lo dataclassesbásico. Herramientas reales y poco comunes que mejoran la calidad de tu código.
1. contextvars— Estado asíncrono-local sin el desorden global
Problema: se desea que un ID de solicitud o un ID de inquilino esté disponible en todas las corrutinas sin tener que pasarlo a todas partes.
Solución: contextvars.ContextVarEs como thread-local, pero para tareas asíncronas.
# python 3.7+
import asyncio
import contextvars
import uuid
REQUEST_ID = contextvars.ContextVar( "request_id" , default= None )
async def handle ():
rid = REQUEST_ID.get()
print ( "manejando" , rid)
await asyncio.sleep( 0.01 )
print ( "listo" , rid)
async def main ():
token = REQUEST_ID. set ( str (uuid.uuid4()))
await handle()
REQUEST_ID.reset(token)
asyncio.run(main())¿Por qué usar la versión senior? Evita fugas accidentales entre tareas que se ejecutan simultáneamente y simplifica enormemente el registro y el seguimiento.
Inconveniente: los cambios se aplican a cada contexto; las tareas secundarias heredan el contexto actual al crearse.
2. functools.singledispatchmethod— Limpieza de la sobrecarga de métodos por tipo
Problema: se desea un comportamiento polimórfico en un método de clase basado en tipos de argumentos, sin if isinstancecadenas feas.
from functools import singledispatchmethod
class Renderer :
@singledispatchmethod
def render ( self, obj ):
raise NotImplementedError
@render.register
def _ ( self, obj: str ):
return f"<p> {obj} </p>"
@render.register
def _ ( self, obj: dict ):
return "" .join( f"<li> {k} : {v} </li>" for k, v in obj.items())
r = Renderer()
print (r.render( "hola" ))
print (r.render({ "a" : 1 , "b" : 2 }))¿Por qué usar senior? Centraliza la lógica de despacho, mantiene los métodos legibles y respeta los tipos.
Consejo: útil para patrones tipo visitante con mínimo código repetitivo.
3. importlib.metadataPuntos de entrada: ecosistemas de plugins ligeros
Problema: quieres plugins sin reinventar el empaquetado ni usar un gestor de plugins externo.
# plugin discoverer (consumer)
from importlib.metadata import entry_points
def load_plugins(group="myapp.plugins"):
eps = entry_points()
for ep in eps.select(group=group):
yield ep.load()
for plugin in load_plugins():
print("loaded", plugin)Los autores de paquetes registran los puntos de entrada en ` setup.cfg.env` o `.env` pyproject.toml. Luego, tu aplicación los descubre en tiempo de ejecución.
¿Por qué es una versión senior? Porque desacopla los puntos de extensión y permite integraciones de terceros de forma limpia.
Nota de compatibilidad: entry_points()La API ha cambiado entre versiones de Python; consulta la documentación o usa importlib_metadatauna versión anterior para versiones antiguas de Python.
4. typing.Protocol+ @runtime_checkable— interfaces con tipado dinámico que realizan comprobaciones de tipos
Problema: se desea una interfaz para tipado estático, pero no se quiere una jerarquía de herencia engorrosa.
from typing import Protocol, runtime_checkable
@runtime_checkable
class HasClose (Protocol):
def close ( self ) -> None: ...
def close_if_possible ( obj: object ):
if isinstance(obj, HasClose):
obj.close()
class Resource :
def close ( self ): print( "cerrado" )
close_if_possible(Resource()) # funciona, no se requiere herencia explícita¿Por qué senior?: permite diseñar API por comportamiento, no por herencia. Ideal para bibliotecas y pruebas.
Inconveniente: isinstancelas comprobaciones requieren @runtime_checkable.
5. TypeGuard— Escribir comprobaciones en tiempo de ejecución que informen sobre los tipos estáticos
Problema: tus funciones auxiliares de inspección en tiempo de ejecución no especifican los tipos para el verificador de tipos.
# Funciona con typing_extensions en versiones antiguas de Python.
Intenta:
from typing import TypeGuard
except Exception:
from typing_extensions import TypeGuard
def is_str_list(x: object ) -> TypeGuard[list[str]]:
return isinstance(x, list) and all(isinstance(i, str) for i in x)
def use(x: object ):
if is_str_list(x):
# Aquí el verificador de tipos sabe que x es list[str]
print( "," . join (x))¿Por qué usar la versión senior? Elimina los falsos positivos en las comprobaciones de tipos y mantiene alineadas la validación en tiempo de ejecución y el tipado estático.
Consejo: úsela para validadores JSON y API que aceptan entradas con tipado flexible.
6. memoryview+ struct.unpack_from— análisis binario sin copia
Problema: analizar grandes flujos binarios sin copiar búferes y sin extensiones de C.
import struct
def parse_header ( buf: memoryview ):
# asumir los primeros 12 bytes: 3 ints
a, b, c = struct.unpack_from( ">III" , buf, 0 )
return a, b, c
data = bytearray ( 1024 )
mv = memoryview (data)
print (parse_header(mv))¿Por qué usar memoria senior? Evita asignaciones adicionales y la presión sobre el recolector de basura. Esencial para telemetría de alto rendimiento, analizadores de red o procesamiento de archivos binarios.
Advertencia: la duración del búfer subyacente es importante; no mantenga vistas de objetos de corta duración.
7. tracemallocInstantáneas: detectan con precisión los puntos críticos de memoria.
Problema: tu programa consume RAM lentamente y pssolo te avisa de que "es más grande".
import tracemalloc
tracemalloc.start()
# ejecuta partes de tu código
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics( "lineno" )[:5]
for stat in top:
print ( stat )¿Por qué es una herramienta senior? Identifica con precisión los sitios de asignación, no solo los sospechosos principales. Úsala para comparar instantáneas antes y después de la ejecución de funciones y detectar fugas de memoria.
Consejo: combínala con filtros y seguimientos de pila para obtener resultados prácticos.
8. weakref.finalize— Limpieza determinista sin los problemas típicos de los destructores
Problema: __del__es frágil (ciclos, cierre del intérprete). Se requiere una limpieza de recursos fiable.
import weakref
import tempfile
import os
class Owner :
def __init__ ( self ):
self.f = tempfile.NamedTemporaryFile(delete= False )
weakref.finalize(self, os.remove, self.f.name)
o = Owner()
# cuando o es recolectado por el recolector de basura, el archivo temporal se elimina¿Por qué son senior? Los finalizadores se ejecutan incluso si __del__hay problemas. Separan la lógica de limpieza de los detalles del ciclo de vida del objeto.
Inconveniente: los finalizadores se ejecutan con el recolector de basura; para una limpieza oportuna, aún se prefieren los administradores de contexto explícitos.
9. dataclassescon slots=Truey frozen=True— DTOs inmutables seguros para la memoria
Problema: muchos objetos pequeños provocan un alto consumo de memoria y mutaciones accidentales.
from dataclasses import dataclass
@dataclass( slots= True , frozen= True )
class Point :
x: float
y: float
p = Point( 1.0 , 2.0 )
# px = 3.0 # genera FrozenInstanceError¿Por qué usar senior?: slotsahorra memoria y acelera el acceso a los atributos; frozenimpone inmutabilidad y un hash más seguro. Se puede combinar para modelos en memoria de alta densidad.
Nota: slots=Truepara dataclasses se requiere Python 3.10 o superior.
10. asyncio.TaskGroup(Concurrencia estructurada) — Gestionar ciclos de vida asíncronos de forma segura
Problema: generación de tareas y pérdida de control de las cancelaciones y excepciones.
import asyncio
async def worker ( n ):
await asyncio.sleep( 0.1 )
if n == 2 :
raise RuntimeError( "boom" )
return n * 2
async def main ():
async with asyncio.TaskGroup() as tg:
for i in range ( 4 ):
tg.create_task(worker(i))
# TaskGroup cancela las tareas hermanas si una falla y vuelve a lanzar la primera excepción
asyncio.run(main())¿Por qué es recomendable usar TaskGroup? La concurrencia estructurada mantiene las tareas acotadas, las excepciones predecibles y los cierres limpios. Si desarrollas sistemas asíncronos y aún no usas TaskGroup, ¡empieza ya!
Gracias por leer Codigo en Casa.