Metaclases: qué son, cómo funcionan y cuándo usarlas

Cuando escribes class Perro:, Python no solo crea un objeto — llama a una fábrica. Esa fábrica es una metaclase: la clase de la clase. Si un objeto es instancia de una clase, entonces una clase es instancia de su metaclase. type es esa metaclase por defecto, y está detrás de cada clase que has definido en tu vida en Python.

El momento exacto en que esto ocurre es cuando el intérprete termina de leer el bloque class. Python recolecta el nombre, las bases, y el namespace del cuerpo, y llama a type(nombre, bases, namespace). El resultado — ese objeto clase que guardas en una variable, que pasas como argumento, que inspeccionas con isinstance — es una instancia de type.

¿Por qué funciona así? Porque en Python todo es un objeto, incluyendo las clases. Y si las clases son objetos, algo tiene que crearlas. type cumple ese rol, y al ser ella misma una clase (instancia de sí misma, lo cual es una singularidad del modelo de objetos), cierra el ciclo de forma consistente.

La pregunta relevante para producción es: ¿cuándo necesitas interceptar esa fabricación? La respuesta honesta es casi nunca. __init_subclass__, decoradores de clase y descriptores resuelven el 95% de los casos con mucho menos ruido. Las metaclases brillan en un nicho preciso: cuando necesitas inspeccionar o transformar la clase en el momento de su definición, antes de que cualquier instancia exista, y además necesitas que ese comportamiento sea heredable automáticamente por todas las subclases sin que el usuario haga nada. Django ORM es el ejemplo canónico. Si tu caso no se parece a eso, probablemente estás sobreingenierando.

Cuando usas una metaclase y algo sale mal, los errores son notoriamente crípticos. Un TypeError en __new__ de tu metaclase puede parecer que viene de la nada porque ocurre en tiempo de definición, no en tiempo de ejecución. El stack trace apunta al class statement, no a ninguna llamada explícita que hayas hecho.

from __future__ import annotations

import inspect
from typing import Any


# ── Metaclase ────────────────────────────────────────────────────────────────

class RegistryMeta(type):
    """
    Metaclase que construye un registro automático de subclases.
    Úsala cuando el framework necesite descubrir implementaciones
    en tiempo de definición, sin que el usuario llame a nada explícito.
    """

    # _registry vive en la metaclase, no en las clases que crea.
    _registry: dict[str, type] = {}

    def __new__(
        mcs,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        **kwargs: Any,
    ) -> "RegistryMeta":
        cls = super().__new__(mcs, name, bases, namespace)

        # No registramos la clase base abstracta, solo sus subclases concretas.
        if bases:
            abstract = namespace.get("__abstract__", False)
            if not abstract:
                RegistryMeta._registry[name] = cls

        return cls

    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        **kwargs: Any,
    ) -> None:
        # __init__ recibe el objeto clase ya creado por __new__.
        # Aquí podemos hacer validaciones post-creación.
        super().__init__(name, bases, namespace)

        # Verificamos que las clases concretas implementen el método requerido.
        if bases and not namespace.get("__abstract__", False):
            if "execute" not in namespace:
                raise TypeError(
                    f"La clase '{name}' debe definir el método 'execute'."
                )

    @classmethod
    def get_registered(mcs) -> dict[str, type]:
        return dict(mcs._registry)


# ── Jerarquía de clases controlada por la metaclase ──────────────────────────

class Command(metaclass=RegistryMeta):
    """Clase base. No se registra, no requiere 'execute' propio."""
    __abstract__ = True

    def execute(self) -> str:  # definido aquí para satisfacer herencia normal
        raise NotImplementedError


class CreateUser(Command):
    def execute(self) -> str:
        return "Creando usuario..."


class DeleteUser(Command):
    def execute(self) -> str:
        return "Eliminando usuario..."


# ── Demostración ──────────────────────────────────────────────────────────────

if __name__ == "__main__":
    registry = RegistryMeta.get_registered()

    print("Clases registradas automáticamente:")
    for class_name, klass in registry.items():
        instance = klass()
        print(f"  {class_name}: {instance.execute()}")

    print()
    print("Cada clase en el registro es instancia de RegistryMeta:")
    for klass in registry.values():
        print(f"  type({klass.__name__}) = {type(klass).__name__}")

    # Intentar definir una subclase sin 'execute' lanza TypeError en tiempo
    # de definición, no cuando alguien intente usar la clase.
    print()
    print("Intentando definir clase inválida...")
    try:
        class BrokenCommand(Command):
            pass  # ❌ falta 'execute'
    except TypeError as e:
        print(f"  TypeError en definición: {e}")

Qué está pasando realmente

RegistryMeta hereda de type. Eso es lo que la convierte en metaclase — no hay decorador mágico, simplemente es una subclase de la fábrica original.

__new__ de la metaclase recibe mcs (la metaclase misma, análogo a cls en classmethod), y los tres ingredientes que Python recolectó del bloque class: nombre, bases y namespace. La llamada a super().__new__() es donde type hace el trabajo pesado — construye el objeto clase real. Lo que haces antes de esa llamada controla los ingredientes; lo que haces después opera sobre el objeto ya construido. Aquí es donde el registro ocurre: guardamos la referencia a cls antes de devolverla.

__init__ de la metaclase recibe el mismo cls que __new__ devolvió, ya completamente construido. La distinción es la misma que en instancias normales: __new__ crea, __init__ inicializa. La validación de la presencia de execute vive aquí porque necesita el objeto clase para inspeccionar su __dict__ (a través de namespace, que es equivalente en este punto).

El detalle de **kwargs en ambos métodos no es decorativo — si alguien pasa keyword arguments en el class statement (como metaclass=RegistryMeta), Python los propaga a través de toda la cadena de llamadas. Sin **kwargs, cualquier argumento extra lanza TypeError.

La comprobación if bases: excluye a Command del registro. Cuando Python procesa class Command(metaclass=RegistryMeta), bases es una tupla vacía porque no heredamos de nada explícito. Eso distingue la clase raíz de las subclases concretas sin necesidad de un flag __abstract__ — aunque el flag añade expresividad cuando la jerarquía es más profunda.

Lo más importante del ejemplo: el error por clase inválida ocurre en tiempo de definición, en la línea del class statement. No cuando alguien instancia BrokenCommand(). Eso es precisamente el valor de las metaclases para ORMs y frameworks declarativos: el contrato se verifica antes de que el sistema esté en ejecución.

Errores que debes conocer

Error: Olvidar **kwargs en __new__ o __init__ de la metaclase cuando hay keyword arguments en el class statement.

# ❌ Wrong
class StrictMeta(type):
    def __new__(mcs, name, bases, namespace):  # sin **kwargs
        return super().__new__(mcs, name, bases, namespace)

class MyClass(metaclass=StrictMeta, some_option=True):  # TypeError aquí
    pass

# ✅ Right
class StrictMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, **kwargs):
        super().__init__(name, bases, namespace)

type.__init__ no acepta kwargs extra, así que hay que absorberlos en tu metaclase y no pasarlos hacia arriba.


Error: Usar una metaclase cuando __init_subclass__ resuelve el problema de forma más limpia.

# ❌ Wrong — metaclase innecesaria solo para validar subclases
class ValidatorMeta(type):
    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if bases and "validate" not in namespace:
            raise TypeError(f"{name} debe implementar 'validate'")

class Base(metaclass=ValidatorMeta):
    pass

# ✅ Right — __init_subclass__ hace exactamente esto sin metaclase
class Base:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if "validate" not in cls.__dict__:
            raise TypeError(f"{cls.__name__} debe implementar 'validate'")

__init_subclass__ se llama automáticamente cuando alguien subclasifica Base, con el mismo resultado y sin tocar el protocolo de metaclases.


Error: Conflicto de metaclases al usar herencia múltiple con clases que tienen metaclases distintas.

# ❌ Wrong — MetaA y MetaB no están relacionadas: TypeError inevitable
class MetaA(type): pass
class MetaB(type): pass

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass

class C(A, B): pass  # TypeError: metaclass conflict

# ✅ Right — la metaclase de C debe ser subclase de ambas
class MetaC(MetaA, MetaB): pass

class C(A, B, metaclass=MetaC): pass

Python exige que la metaclase de la subclase sea subclase de las metaclases de todas sus bases — el mismo principio del MRO aplicado un nivel más arriba.


Control Block

134

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio