En Python, todo es un objeto, incluidas las clases. Y como cualquier objeto, las clases tienen un tipo: ese tipo es su metaclase. Por defecto, la metaclase de todas las clases es type. Las metaclases permiten controlar cómo se crean las clases, añadir métodos automáticamente, validar la herencia o implementar registros de plugins sin que el usuario tenga que hacer nada explícito.
Crear clases dinámicamente con type()
type tiene dos usos: cuando se llama con un argumento devuelve el tipo del objeto; cuando se llama con tres argumentos crea una clase nueva en tiempo de ejecución.
# type(nombre, bases, dict_de_atributos)
Animal = type('Animal', (), {
'especie': 'desconocida',
'hablar': lambda self: print(f"Soy un {self.especie}")
})
Perro = type('Perro', (Animal,), {
'especie': 'Canis lupus familiaris'
})
rex = Perro()
rex.hablar() # Soy un Canis lupus familiaris
print(type(Perro)) # <class 'type'>
Escribir una metaclase con __new__
Una metaclase es una clase que hereda de type. Su __new__ recibe el nombre, las bases y el namespace de la clase que se está creando:
class SingletonMeta(type):
_instancias: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instancias:
cls._instancias[cls] = super().__call__(*args, **kwargs)
return cls._instancias[cls]
class Configuracion(metaclass=SingletonMeta):
def __init__(self):
self.debug = False
self.host = "localhost"
c1 = Configuracion()
c2 = Configuracion()
print(c1 is c2) # True siempre la misma instancia
Metaclase con __new__: validar la clase al crearla
class ValidarMetodos(type):
def __new__(mcs, nombre, bases, namespace):
for clave, valor in namespace.items():
if callable(valor) and not clave.startswith('_'):
if not valor.__doc__:
raise TypeError(
f"El método '{nombre}.{clave}' debe tener docstring"
)
return super().__new__(mcs, nombre, bases, namespace)
class API(metaclass=ValidarMetodos):
def obtener_usuarios(self):
"""Devuelve la lista de usuarios activos."""
return []
def borrar_usuario(self, uid: int):
"""Elimina el usuario con el id dado."""
pass
# Esto lanzaría TypeError:
# class APIRota(metaclass=ValidarMetodos):
# def sin_doc(self):
# pass
__init_subclass__: alternativa más sencilla
Desde Python 3.6, __init_subclass__ permite reaccionar a la creación de subclases sin necesidad de escribir una metaclase completa. Es la opción recomendada para la mayoría de los casos:
class Forma:
subclases: list = []
def __init_subclass__(cls, color: str = "negro", **kwargs):
super().__init_subclass__(**kwargs)
cls.color = color
Forma.subclases.append(cls)
print(f"Nueva subclase registrada: {cls.__name__} (color={color})")
class Circulo(Forma, color="rojo"):
pass
class Cuadrado(Forma, color="azul"):
pass
print(Forma.subclases) # [Circulo, Cuadrado]
print(Circulo.color) # rojo
__class_getitem__: habilitar MiClase[tipo]
__class_getitem__ se llama cuando escribes MiClase[algo]. Es lo que permite la sintaxis de genéricos en Python:
class Pila:
def __class_getitem__(cls, tipo):
nombre = f"Pila[{tipo.__name__ if hasattr(tipo, '__name__') else tipo}]"
return type(nombre, (cls,), {'_tipo': tipo})
def __init__(self):
self._datos: list = []
def push(self, valor):
if not isinstance(valor, self.__class__._tipo):
raise TypeError(f"Se esperaba {self.__class__._tipo.__name__}")
self._datos.append(valor)
def pop(self):
return self._datos.pop()
PilaInt = Pila[int]
p = PilaInt()
p.push(1)
p.push(2)
p.push("hola") # TypeError: Se esperaba int
Sistema de registro automático de plugins
El caso de uso más habitual de las metaclases en producción es un registro de plugins: cada subclase que alguien defina se registra automáticamente sin que tenga que llamar a ninguna función.
class RegistroPlugin(type):
_registro: dict[str, type] = {}
def __new__(mcs, nombre, bases, namespace):
cls = super().__new__(mcs, nombre, bases, namespace)
if bases: # no registrar la clase base
alias = namespace.get('nombre', nombre.lower())
mcs._registro[alias] = cls
return cls
@classmethod
def obtener(mcs, nombre: str) -> type:
try:
return mcs._registro[nombre]
except KeyError:
raise ValueError(f"Plugin '{nombre}' no registrado") from None
class Exportador(metaclass=RegistroPlugin):
"""Clase base de exportadores."""
def exportar(self, datos) -> bytes:
raise NotImplementedError
class ExportadorJSON(Exportador):
nombre = 'json'
def exportar(self, datos) -> bytes:
import json
return json.dumps(datos).encode()
class ExportadorCSV(Exportador):
nombre = 'csv'
def exportar(self, datos) -> bytes:
import csv, io
buf = io.StringIO()
w = csv.writer(buf)
for fila in datos:
w.writerow(fila)
return buf.getvalue().encode()
# En tiempo de ejecución, cargamos el plugin por nombre
plugin = RegistroPlugin.obtener('json')
resultado = plugin().exportar({"clave": "valor"})
print(resultado) # b'{"clave": "valor"}'
¿Metaclase o __init_subclass__?
Usa __init_subclass__ cuando solo necesitas reaccionar a la creación de subclases: es más sencillo, más legible y evita conflictos de metaclases cuando hay herencia múltiple. Reserva las metaclases para cuando necesitas modificar cómo se crea la clase (__new__), interceptar el proceso completo de creación o mantener registros a nivel de metaclase (como el ejemplo de plugins anterior).
