Incluso mi yo del pasado me lo agradecería.

Lo admito. Durante años, escribí código Python que funcionaba, pero que no era precisamente bonito. Mis variables estaban por todas partes, mis funciones parecían el monstruo de Frankenstein y, al cabo de unas semanas, leer mi propio código era como descifrar jeroglíficos.

Pero aquí está la buena noticia: no es necesario reescribir todo desde cero para que tu código sea más limpio. Pequeños trucos de refactorización, bien hechos, convierten los espaguetis en pasta con estrella Michelin.

Te voy a enseñar nueve trucos de refactorización de Python que me cambiaron la vida (y me ahorraron innumerables dolores de cabeza en el futuro).

1. Refactoriza los decoradores repetidos con una fábrica de decoradores

Apilar decoradores en docenas de funciones es ruidoso. Crea una fábrica que los genere dinámicamente.

from functools import wraps

def logged(level):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            print(f"[{level}] Calling {fn.__name__}")
            return fn(*args, **kwargs)
        return wrapper
    return decorator

@logged("INFO")
def process(): pass

Se acabó copiar y pegar @logger.info, @logger.debug, etc. en todo el código base.

2. Sustituye Reflection Hell por __init_subclass__ Hooks.

En lugar de registrar manualmente las subclases, conecta al ciclo de vida de creación de clases de Python.

class PluginBase:
    registry = {}
    def __init_subclass__(cls, key=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if key:
            PluginBase.registry[key] = cls

class CSVLoader(PluginBase, key="csv"): ...
class JSONLoader(PluginBase, key="json"): ...

print(PluginBase.registry)
# {'csv': <class '__main__.CSVLoader'>, 'json': <class '__main__.JSONLoader'>}

Ahora, solo tienes que crear una subclase y automáticamente estarás «conectado».

3. Utiliza la reescritura AST para aplicar estilos u optimizar el código

¿Por qué confiar solo en los linters cuando puedes reescribir el código en el momento de la importación?

import ast, inspect

def inline_constants(fn):
    src = inspect.getsource(fn)
    tree = ast.parse(src)
    for node in ast.walk(tree):
        if isinstance(node, ast.Name) and node.id == "PI":
            node.id = "3.14159"
    code = compile(tree, filename="<ast>", mode="exec")
    ns = {}
    exec(code, fn.__globals__, ns)
    return ns[fn.__name__]

PI = 3.14159

@inline_constants
def area(r): return PI * r * r

print(area(10))  # 314.159

Acabas de reescribir tu propia función antes de la ejecución.

4. Elimina el código repetitivo con descriptores

En lugar de escribir getters/setters para cada atributo, utiliza un descriptor reutilizable.

class Positive:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("Must be positive")
        obj.__dict__[self.name] = value

class Account:
    balance = Positive()

a = Account()
a.balance = 100   # works
a.balance = -50   # boom!

Los descriptores son el motor oculto detrás de @property.

5. Reestructura las condiciones en funciones de envío único

En lugar de un mega if/elif, envía por tipo de forma elegante.

from functools import singledispatch

@singledispatch
def to_json(val): raise NotImplementedError

@to_json.register(str)
def _(val): return f'"{val}"'

@to_json.register(int)
def _(val): return str(val)

print(to_json("hello"))  # "hello"
print(to_json(42))       # 42

Extensibilidad limpia sin ninguna modificación de la función original.

6. Reemplazar la gestión manual del contexto con ContextDecorator

Convierta cualquier lógica de configuración/desmontaje en un gestor de contexto y un decorador.

from contextlib import ContextDecorator

class trace(ContextDecorator):
    def __enter__(self): print("Start"); return self
    def __exit__(self, *exc): print("End"); return False

@trace()
def process(): print("Work")

with trace():
    print("Inside block")
process()

La misma lógica, dos casos de uso diferentes.

7. Intercambio de árboles de herencia con protocolos (PEP 544)

Tipado estático por duck typing > jerarquías de clases rígidas.

from typing import Protocol

class Reader(Protocol):
    def read(self) -> str: ...

class FileReader:
    def read(self): return "file contents"

class APIReader:
    def read(self): return "api data"

def process(r: Reader): print(r.read())

process(FileReader())
process(APIReader())

Los protocolos desacoplan los contratos de la implementación, lo que supone un gran avance para los sistemas de gran tamaño.

8. Lógica de intercambio en caliente con importaciones dinámicas

Refactorizar bloques gigantes if ENV == «prod»: ... else: en inyección de módulos en tiempo de ejecución.

import importlib, os

env = os.getenv("APP_ENV", "dev")
config = importlib.import_module(f"config_{env}")

print(config.DB_URI)

Una importación limpia en lugar de condicionales desordenados por todas partes.

9. Utiliza metaclases para inyectar comportamiento automáticamente

Sí, son peligrosas. Pero cuando se utilizan con moderación, eliminan el código repetitivo.

class AutoRepr(type):
    def __new__(cls, name, bases, dct):
        def __repr__(self):
            attrs = ", ".join(f"{k}={v!r}" for k,v in self.__dict__.items())
            return f"{name}({attrs})"
        dct["__repr__"] = __repr__
        return super().__new__(cls, name, bases, dct)

class User(metaclass=AutoRepr):
    def __init__(self, name, age): self.name, self.age = name, age

print(User("Alice", 30))
# User(name='Alice', age=30)

Acabas de dar a cada subclase un __repr__

¡Gracias por Leer Código en Casa!