Mucha gente piensa que la metaprogramación en Python complica innecesariamente el código, pero si lo usa correctamente, puede implementar rápida y elegantemente patrones de diseño complejos. Además, los marcos de Python conocidos como Django, DRF y SQLAlchemy utilizan metaclases para proporcionar una fácil extensibilidad y una reutilización de código fácil.

En este artículo, le diré por qué no debe tener miedo de usar la metaprogramación en sus proyectos y mostrarle para qué tareas es más adecuada. Puede obtener más información sobre las opciones de metaprogramación en el curso avanzado de Python .
Para comenzar, recordemos los conceptos básicos de la metaprogramación en Python. No será superfluo agregar que todo lo que se escribe a continuación se aplica a Python versión 3.5 y superior.
Un recorrido rápido por el modelo de datos de Python
Entonces, todos sabemos que todo en Python es un objeto, y no es ningún secreto que para cada objeto hay una cierta clase por la cual se generó, por ejemplo:
>>> def f(): pass >>> type(f) <class 'function'>
El tipo del objeto o la clase por la cual se generó el objeto se puede determinar utilizando la función de tipo incorporada, que tiene una firma de llamada bastante interesante (hablaremos de ello un poco más adelante). Se puede lograr el mismo efecto derivando el atributo __class__
en cualquier objeto.
Entonces, para crear funciones, function
alguna function
clase function
. Veamos qué podemos hacer con él. Para hacer esto, tome el espacio en blanco del módulo de tipos incorporado:
>>> from types import FunctionType >>> FunctionType <class 'function'> >>> help(FunctionType) class function(object) | function(code, globals[, name[, argdefs[, closure]]]) | | Create a function object from a code object and a dictionary. | The optional name string overrides the name from the code object. | The optional argdefs tuple specifies the default argument values. | The optional closure tuple supplies the bindings for free variables.
Como podemos ver, cualquier función en Python es una instancia de la clase descrita anteriormente. Ahora tratemos de crear una nueva función sin recurrir a su declaración a través de def
. Para hacer esto, necesitamos aprender a crear objetos de código utilizando la función de compilación integrada en el intérprete:
Genial Con la ayuda de metaherramientas, aprendimos cómo crear funciones sobre la marcha, pero en la práctica ese conocimiento rara vez se usa. Ahora echemos un vistazo a cómo se crean los objetos de clase y los objetos de instancia de estas clases:
>>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'>
Es bastante obvio que la clase User
se usa para crear una instancia de user
, es mucho más interesante observar la clase de type
, que se usa para crear la clase User
sí. Aquí pasaremos a la segunda opción de llamar a la función de type
incorporada, que en combinación es una metaclase para cualquier clase en Python. Una metaclase es, por definición, una clase cuya instancia es otra clase. Las metaclases nos permiten personalizar el proceso de creación de una clase y controlar parcialmente el proceso de creación de una instancia de una clase.
Según la documentación, la segunda variante del type(name, bases, attrs)
firma type(name, bases, attrs)
- devuelve un nuevo tipo de datos o, si es simple - una nueva clase, y el atributo de name
convierte en el atributo __name__
de la clase devuelta, bases
- la lista de clases primarias estará disponible como __bases__
, Bueno, attrs
, un objeto tipo dict que contiene todos los atributos y métodos de la clase, entrará en __dict__
. El principio de la función se puede describir como un pseudocódigo simple en Python:
type(name, bases, attrs) ~ class name(bases): attrs
Veamos cómo puedes, usando solo la llamada de type
, construir una clase completamente nueva:
>>> User = type('User', (), {}) >>> User <class '__main__.User'>
Como puede ver, no necesitamos usar la palabra clave de class
para crear una nueva clase, la función de type
no funciona, ahora veamos un ejemplo más complicado:
class User: def __init__(self, name): self.name = name class SuperUser(User): """Encapsulate domain logic to work with super users""" group_name = 'admin' @property def login(self): return f'{self.group_name}/{self.name}'.lower()
Como puede ver en los ejemplos anteriores, la descripción de clases y funciones usando las palabras clave class
y def
es simplemente azúcar sintáctica y cualquier tipo de objeto puede ser creado mediante llamadas ordinarias a funciones integradas. Y ahora, finalmente, hablemos sobre cómo puede usar la creación dinámica de clases en proyectos reales.
A veces necesitamos validar información del usuario o de otras fuentes externas de acuerdo con un esquema de datos previamente conocido. Por ejemplo, queremos cambiar el formulario de inicio de sesión del usuario desde el panel de administración: eliminar y agregar campos, cambiar la estrategia de su validación, etc.
Para ilustrar, intentemos crear dinámicamente un formulario de Django , cuya descripción del esquema se almacena en el siguiente formato json
:
{ "fist_name": { "type": "str", "max_length": 25 }, "last_name": { "type": "str", "max_length": 30 }, "age": { "type": "int", "min_value": 18, "max_value": 99 } }
Ahora, según la descripción anterior, cree un conjunto de campos y un nuevo formulario utilizando la función de type
que ya conocemos:
import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, }
Genial Ahora puede transferir el formulario creado a la plantilla y representarlo para el usuario. El mismo enfoque se puede utilizar con otros marcos para la validación y presentación de datos ( serializadores DRF , malvaviscos y otros).
Arriba, observamos la metaclase de type
ya "terminada", pero con mayor frecuencia en el código creará sus propias metaclases y las usará para configurar la creación de nuevas clases y sus instancias. En el caso general, el "espacio en blanco" de una metaclase se ve así:
class MetaClass(type): """ : mcs – , <__main__.MetaClass> name – , , , "User" bases – -, (SomeMixin, AbstractUser) attrs – dict-like , cls – , <__main__.User> extra_kwargs – keyword- args kwargs – """ def __new__(mcs, name, bases, attrs, **extra_kwargs): return super().__new__(mcs, name, bases, attrs) def __init__(cls, name, bases, attrs, **extra_kwargs): super().__init__(cls) @classmethod def __prepare__(mcs, cls, bases, **extra_kwargs): return super().__prepare__(mcs, cls, bases, **kwargs) def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs)
Para usar esta metaclase para configurar la clase User
, se usa la siguiente sintaxis:
class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name
Lo más interesante es el orden en que el intérprete de Python llama al metametodo metaclase en el momento en que se crea la clase:
- El intérprete determina y encuentra las clases principales para la clase actual (si existe).
- El intérprete define una metaclase (
MetaClass
en nuestro caso). - Se
MetaClass.__prepare__
método MetaClass.__prepare__
: debe devolver un objeto tipo dict en el que se escribirán los atributos y métodos de la clase. Después de eso, el objeto se pasará al MetaClass.__new__
través del argumento attrs
. Hablaremos sobre el uso práctico de este método un poco más adelante en los ejemplos. - El intérprete lee el cuerpo de la clase
User
y genera parámetros para pasarlos a la metaclase MetaClass
. - El método
MetaClass.__new__
se MetaClass.__new__
- el método MetaClass.__new__
, devuelve el objeto de clase creado. Ya nos reunimos con los argumentos name
, bases
y attrs
cuando los pasamos a la función type
, y hablaremos sobre el parámetro **extra_kwargs
un poco más tarde. Si el tipo del argumento attrs
se cambió usando __prepare__
, entonces debe convertirse a un dict
antes de pasarlo a la llamada al método super()
. - El método
MetaClass.__init__
se MetaClass.__init__
: el método inicializador con el que puede agregar atributos y métodos adicionales al objeto de clase en la clase. En la práctica, se usa en casos donde las metaclases se heredan de otras metaclases, de lo contrario, todo lo que se puede hacer en __init__
se hace mejor en __new__
. Por ejemplo, el parámetro __slots__
solo se puede establecer en el método __new__
escribiéndolo en el objeto attrs
. - En este paso, la clase se considera creada.
Ahora cree una instancia de nuestra clase de User
y observe la cadena de llamadas:
user = User(name='Alyosha')
- En el momento de llamar al
User(...)
intérprete llama al MetaClass.__call__(name='Alyosha')
, donde pasa el objeto de clase y pasan los argumentos. MetaClass.__call__
calls User.__new__(name='Alyosha')
- un método constructor que crea y devuelve una instancia de la clase User
- A continuación,
MetaClass.__call__
llama a User.__init__(name='Alyosha')
, un método inicializador que agrega nuevos atributos a la instancia creada. MetaClass.__call__
devuelve la instancia creada e inicializada de la clase User
.- En este punto, se considera creada una instancia de la clase.
Esta descripción, por supuesto, no cubre todos los matices del uso de metaclases, pero es suficiente para comenzar a usar la metaprogramación para implementar algunos patrones arquitectónicos. ¡Adelante a los ejemplos!
Clases abstractas
Y el primer ejemplo se puede encontrar en la biblioteca estándar: ABCMeta : una metaclase le permite declarar cualquiera de nuestros resúmenes de clase y obligar a todos sus descendientes a implementar métodos, propiedades y atributos predefinidos, así que mire:
from abc import ABCMeta, abstractmethod class BasePlugin(metaclass=ABCMeta): """ supported_formats run """ @property @abstractmethod def supported_formats(self) -> list: pass @abstractmethod def run(self, input_data: dict): pass
Si no se implementan todos los métodos y atributos abstractos en el heredero, cuando intentamos crear una instancia de la clase heredera, obtenemos un TypeError
:
class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin()
El uso de clases abstractas ayuda a corregir inmediatamente la interfaz de la clase base y evitar futuros errores de herencia, por ejemplo, errores tipográficos en el nombre de un método anulado.
Sistema de complemento de registro automático
Muy a menudo, la metaprogramación se usa para implementar varios patrones de diseño. Casi cualquier marco conocido utiliza metaclases para crear objetos de registro . Dichos objetos almacenan enlaces a otros objetos y permiten que se reciban rápidamente en cualquier parte del programa. Considere un ejemplo simple de registro automático de complementos para reproducir archivos multimedia de varios formatos.
Implementación de metaclase:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
Y aquí están los complementos mismos, tomaremos la implementación de BasePlugin
del ejemplo anterior:
class BasePlugin(metaclass=RegistryMeta): ... class VideoPlugin(BasePlugin): supported_formats = ['mpg', 'mov'] def run(self): ... class AudioPlugin(BasePlugin): supported_formats = ['mp3', 'flac'] def run(self): ...
Después de ejecutar este código, el intérprete registrará 4 formatos y 2 complementos en nuestro registro que pueden procesar estos formatos:
>>> RegistryMeta.show_registry() {'flac': <class '__main__.AudioPlugin'>, 'mov': <class '__main__.VideoPlugin'>, 'mp3': <class '__main__.AudioPlugin'>, 'mpg': <class '__main__.VideoPlugin'>} >>> plugin_class = RegistryMeta.get_plugin('mov') >>> plugin_class <class '__main__.VideoPlugin'> >>> plugin_class().run() Processing video...
Vale la pena señalar un matiz más interesante de trabajar con metaclases, gracias al orden de resolución de método no obvio, podemos llamar al método show_registry
no solo en la clase RegistyMeta
, sino en cualquier otra clase de la que sea una metaclase:
>>> AudioPlugin.get_plugin('avi')
Usando metaclases, puede usar nombres de atributos de clase como metadatos para otros objetos. Nada esta claro? Pero estoy seguro de que ya has visto este enfoque muchas veces, por ejemplo, declaración declarativa de campos modelo en Django:
class Book(models.Model): title = models.Charfield(max_length=250)
En el ejemplo anterior, title
es el nombre del identificador de Python, también se usa para nombrar la columna en la tabla del book
, aunque no lo indicamos explícitamente en ninguna parte. Sí, esa "magia" se puede realizar con la ayuda de la metaprogramación. Implementemos, por ejemplo, un sistema para transmitir errores de aplicación al front-end, de modo que cada mensaje tenga un código legible que pueda usarse para traducir el mensaje a otro idioma. Entonces, tenemos un objeto de mensaje que se puede convertir a json
:
class Message: def __init__(self, text, code=None): self.text = text self.code = code def to_json(self): return json.dumps({'text': self.text, 'code': self.code})
Todos nuestros mensajes de error se almacenarán en un "espacio de nombres" separado:
class Messages: not_found = Message('Resource not found') bad_request = Message('Request body is invalid') ... >>> Messages.not_found.to_json() {"text": "Resource not found", "code": null}
Ahora queremos que el code
no sea null
, pero no not_found
, para esto escribimos la siguiente metaclase:
class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items():
Veamos cómo se ven nuestras publicaciones ahora:
>>> Messages.not_found.to_json() {"text": "Resource not found", "code": "not_found"} >>> Messages.bad_request.to_json() {"text": "Request body is invalid", "code": "bad_request"}
Lo que necesitas! Ahora sabe qué hacer para que por el formato de datos pueda encontrar fácilmente el código que los procesa.
Otro caso común es el almacenamiento en caché de los datos estáticos en la etapa de creación de la clase, para no perder el tiempo calculando mientras se ejecuta la aplicación. Además, algunos datos se pueden actualizar al crear nuevas instancias de clases, por ejemplo, un contador del número de objetos creados.
¿Cómo se puede usar esto? Supongamos que está desarrollando un marco para crear informes y tablas y tiene un objeto de este tipo:
class Row(metaclass=MetaRow): name: str age: int ... def __init__(self, **kwargs): self.counter = None for attr, value in kwargs.items(): setattr(self, attr, value) def __str__(self): out = [self.counter]
Queremos guardar y aumentar el contador al crear una nueva fila, y también queremos generar el encabezado de la tabla resultante de antemano. Metaclase al rescate!
class MetaRow(type):
Aquí hay que aclarar dos cosas:
- La clase
Row
no tiene atributos de clase con el name
y la age
los nombres: son anotaciones de tipo , por lo que no están en las attrs
diccionario attrs
, y para obtener una lista de campos, utilizamos el __annotations__
clase __annotations__
. - Se
cls.row_count += 1
operación cls.row_count += 1
debía confundirlo: ¿cómo es eso? Después de todo, cls
es una clase Row
; no tiene el atributo row_count
. Todo es cierto, pero como expliqué anteriormente, si la clase creada no tiene un atributo o método al que están tratando de llamar, entonces el intérprete va más allá de la cadena de clases base; si no hay ninguno en ellos, se realiza una búsqueda en la metaclase. En tales casos, para no confundir a nadie, es mejor usar otro registro: MetaRow.row_count += 1
.
Vea cuán elegantemente puede mostrar ahora toda la tabla:
rows = [ Row(name='Valentin', age=25), Row(name='Sergey', age=33), Row(name='Gosha'), ] print(' | '.join(Row.__header__)) for row in rows: print(row)
№ | age | name 1 | 25 | Valentin 2 | 33 | Sergey 3 | N/A | Gosha
Por cierto, mostrar y trabajar con una tabla se puede encapsular en una clase de Sheet
separada.
Continuará ...
En la siguiente parte de este artículo, describiré cómo usar las metaclases para depurar el código de su aplicación, cómo parametrizar la creación de una metaclase y mostraré ejemplos básicos del uso del método __prepare__
. Estén atentos!
Con más detalle sobre metaclases y descriptores en Python, lo contaré en el marco de Advanced Python .