Muitas pessoas pensam que a metaprogramação no Python complica desnecessariamente o código, mas se você o usar corretamente, poderá implementar rápida e elegantemente padrões de design complexos. Além disso, estruturas conhecidas do Python, como Django, DRF e SQLAlchemy, usam metaclasses para fornecer extensibilidade fácil e reutilização fácil de código.

Neste artigo, explicarei por que você não deve ter medo de usar a metaprogramação em seus projetos e mostrar para quais tarefas ela é mais adequada. Você pode aprender mais sobre as opções de metaprogramação no curso Advanced Python .
Para começar, vamos relembrar o básico da metaprogramação no Python. Não será supérfluo acrescentar que tudo o que está escrito abaixo se refere à versão 3.5 e superior do Python.
Um rápido tour pelo modelo de dados Python
Portanto, todos sabemos que tudo em Python é um objeto, e não é segredo que para cada objeto existe uma certa classe pela qual foi gerada, por exemplo:
>>> def f(): pass >>> type(f) <class 'function'>
O tipo do objeto ou a classe pela qual o objeto foi gerado pode ser determinado usando a função de tipo interna, que possui uma assinatura de chamada bastante interessante (falaremos sobre isso mais adiante). O mesmo efeito pode ser obtido derivando o atributo __class__
em qualquer objeto.
Portanto, para criar funções, uma certa function
classe function
. Vamos ver o que podemos fazer com isso. Para fazer isso, retire o espaço em branco do módulo de tipos interno:
>>> 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, qualquer função no Python é uma instância da classe descrita acima. Vamos agora tentar criar uma nova função sem recorrer à sua declaração através de def
. Para fazer isso, precisamos aprender como criar objetos de código usando a função de compilação incorporada ao intérprete:
Ótimo! Com a ajuda de meta-ferramentas, aprendemos a criar funções dinamicamente, mas na prática esse conhecimento raramente é usado. Agora, vamos dar uma olhada em como os objetos de classe e os objetos de instância dessas classes são criados:
>>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'>
É bastante óbvio que a classe User
é usada para criar uma instância do user
, é muito mais interessante olhar para a classe type
, que é usada para criar a própria classe User
. Aqui, voltaremos à segunda opção de chamar a função de type
interno, que em combinação é uma metaclasse para qualquer classe no Python. Uma metaclasse é, por definição, uma classe cuja instância é outra classe. As metaclasses nos permitem personalizar o processo de criação de uma classe e controlar parcialmente o processo de criação de uma instância de uma classe.
De acordo com a documentação, a segunda variante do type(name, bases, attrs)
assinatura type(name, bases, attrs)
- retorna um novo tipo de dados ou, se de maneira simples - uma nova classe, e o atributo name
se torna o atributo __name__
da classe retornada, bases
- a lista de classes pai estará disponível como __bases__
, Bem, attrs
- um objeto semelhante a dict contendo todos os atributos e métodos da classe, entrará em __dict__
. O princípio da função pode ser descrito como um pseudo-código simples em Python:
type(name, bases, attrs) ~ class name(bases): attrs
Vamos ver como você pode, usando apenas a chamada de type
, construir uma classe completamente nova:
>>> User = type('User', (), {}) >>> User <class '__main__.User'>
Como você pode ver, não precisamos usar a palavra-chave class
para criar uma nova classe, a função type
fica sem ela, agora vamos ver um exemplo mais 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 você pode ver nos exemplos acima, a descrição de classes e funções usando as palavras-chave class
e def
é apenas um açúcar sintático e qualquer tipo de objeto pode ser criado por chamadas comuns a funções internas. E agora, finalmente, vamos falar sobre como você pode usar a criação dinâmica de classes em projetos reais.
Às vezes, precisamos validar as informações do usuário ou de outras fontes externas de acordo com um esquema de dados conhecido anteriormente. Por exemplo, queremos alterar o formulário de login do usuário no painel de administração - excluir e adicionar campos, alterar a estratégia de validação etc.
Para ilustrar, vamos tentar criar dinamicamente um formulário Django , cuja descrição do esquema é armazenada no seguinte 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 } }
Agora, com base na descrição acima, crie um conjunto de campos e um novo formulário usando a função de type
que já conhecemos:
import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, }
Ótimo! Agora você pode transferir o formulário criado para o modelo e renderizá-lo para o usuário. A mesma abordagem pode ser usada com outras estruturas para validação e apresentação de dados ( serializadores DRF , marshmallow e outros).
Acima, vimos a metaclasse do type
"terminado", mas na maioria das vezes no código você criará suas próprias metaclasses e as usará para configurar a criação de novas classes e suas instâncias. No caso geral, o "espaço em branco" de uma metaclasse é assim:
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 essa metaclasse para configurar a classe User
, a seguinte sintaxe é usada:
class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name
O mais interessante é a ordem na qual o interpretador Python chama o metamétodo da metaclasse no momento em que a própria classe é criada:
- O intérprete determina e localiza as classes pai da classe atual (se houver).
- O intérprete define uma metaclasse (
MetaClass
no nosso caso). - O método
MetaClass.__prepare__
é MetaClass.__prepare__
- ele deve retornar um objeto semelhante ao MetaClass.__prepare__
no qual os atributos e métodos da classe serão gravados. Depois disso, o objeto será passado para o MetaClass.__new__
através do argumento attrs
. Falaremos sobre o uso prático desse método um pouco mais adiante nos exemplos. - O intérprete lê o corpo da classe
User
e gera parâmetros para transmiti-los à metaclasse MetaClass
. - O método
MetaClass.__new__
é MetaClass.__new__
- o método MetaClass.__new__
, retorna o objeto de classe criado. Já conhecemos os argumentos name
, bases
e attrs
quando os passamos para a função type
, e falaremos sobre o parâmetro **extra_kwargs
um pouco mais tarde. Se o tipo do argumento attrs
foi alterado usando __prepare__
, ele deve ser convertido em um dict
antes de passá-lo para a chamada do método super()
. - O método
MetaClass.__init__
é MetaClass.__init__
- o método inicializador com o qual você pode adicionar atributos e métodos adicionais ao objeto de classe na classe. Na prática, é usado nos casos em que as metaclasses são herdadas de outras metaclasses, caso contrário, tudo o que pode ser feito em __init__
é melhor feito em __new__
. Por exemplo, o parâmetro __slots__
só pode ser definido no método __new__
gravando-o no objeto attrs
. - Nesta etapa, a classe é considerada criada.
Agora crie uma instância da nossa classe User
e observe a cadeia de chamadas:
user = User(name='Alyosha')
- No momento da chamada do
User(...)
intérprete chama o MetaClass.__call__(name='Alyosha')
, onde passa o objeto de classe e os argumentos passados. MetaClass.__call__
chama User.__new__(name='Alyosha')
- um método construtor que cria e retorna uma instância da classe User
- Em seguida,
MetaClass.__call__
chama User.__init__(name='Alyosha')
- um método inicializador que adiciona novos atributos à instância criada. MetaClass.__call__
retorna a instância criada e inicializada da classe User
.- Neste ponto, uma instância da classe é considerada criada.
Essa descrição, é claro, não cobre todas as nuances do uso de metaclasses, mas é suficiente começar a usar a metaprogramação para implementar alguns padrões arquiteturais. Encaminhar para os exemplos!
Classes abstratas
E o primeiro exemplo pode ser encontrado na biblioteca padrão: ABCMeta - uma metaclasse permite que você declare qualquer resumo de nossa classe e force todos os seus descendentes a implementar métodos, propriedades e atributos predefinidos;
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
Se todos os métodos e atributos abstratos não forem implementados no herdeiro, quando tentarmos criar uma instância da classe herdeiro, obteremos um TypeError
:
class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin()
O uso de classes abstratas ajuda a corrigir imediatamente a interface da classe base e a evitar futuros erros de herança, por exemplo erros de digitação no nome de um método substituído.
Sistema de plug-in de registro automático
Muitas vezes, a metaprogramação é usada para implementar vários padrões de design. Quase qualquer estrutura conhecida usa metaclasses para criar objetos de registro . Tais objetos armazenam links para outros objetos e permitem que sejam recebidos rapidamente em qualquer lugar do programa. Considere um exemplo simples de registro automático de plug-ins para reproduzir arquivos de mídia de vários formatos.
Implementação de metaclasse:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
E aqui estão os próprios plug-ins, pegaremos a implementação BasePlugin
do exemplo 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): ...
Após executar este código, o intérprete registrará 4 formatos e 2 plugins em nosso registro que podem processar estes 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 a pena notar mais uma nuance interessante de trabalhar com metaclasses, graças à ordem de resolução do método não óbvio, podemos chamar o método show_registry
não apenas na classe RegistyMeta
, mas em qualquer outra classe da qual seja uma metaclasse:
>>> AudioPlugin.get_plugin('avi')
Usando metaclasses, você pode usar nomes de atributos de classe como metadados para outros objetos. Nada está claro? Mas tenho certeza que você já viu essa abordagem muitas vezes, por exemplo, declaração declarativa dos campos de modelo no Django:
class Book(models.Model): title = models.Charfield(max_length=250)
No exemplo acima, title
é o nome do identificador Python, também é usado para nomear a coluna na tabela de book
, embora não tenhamos indicado isso explicitamente em nenhum lugar. Sim, essa "mágica" pode ser realizada com a ajuda da metaprogramação. Vamos, por exemplo, implementar um sistema para transmitir erros de aplicativos ao front-end, para que cada mensagem tenha um código legível que possa ser usado para traduzir a mensagem em outro idioma. Portanto, temos um objeto de mensagem que pode ser convertido em 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})
Todas as nossas mensagens de erro serão armazenadas em um "espaço para nome" 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}
Agora queremos que o code
torne não null
, mas não not_found
, para isso, escrevemos a seguinte metaclasse:
class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items():
Vamos ver como ficam as nossas postagens:
>>> 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"}
O que você precisa! Agora você sabe o que fazer para que, pelo formato dos dados, encontre facilmente o código que os processa.
Outro caso comum é armazenar em cache qualquer dado estático no estágio de criação da classe, para não perder tempo calculando-o enquanto o aplicativo está em execução. Além disso, alguns dados podem ser atualizados ao criar novas instâncias de classes, por exemplo, um contador do número de objetos criados.
Como isso pode ser usado? Suponha que você esteja desenvolvendo uma estrutura para criar relatórios e tabelas e tenha um objeto como esse:
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 salvar e aumentar o contador ao criar uma nova linha e também queremos gerar o cabeçalho da tabela resultante com antecedência. Metaclasse para o resgate!
class MetaRow(type):
Duas coisas precisam ser esclarecidas aqui:
- A classe
Row
não possui atributos de classe com os nomes name
e age
- são anotações de tipo , portanto não estão nas chaves do dicionário attrs
e, para obter uma lista de campos, usamos o __annotations__
classe __annotations__
. - A operação
cls.row_count += 1
deveria enganar você: como assim? Afinal, cls
é uma classe Row
; não possui o atributo row_count
. Tudo é verdade, mas, como expliquei acima - se a classe criada não possui um atributo ou método que eles estão tentando chamar, o intérprete vai além na cadeia de classes base - se não houver nenhuma, uma pesquisa é feita na metaclasse. Nesses casos, para não confundir ninguém, é melhor usar outro registro: MetaRow.row_count += 1
.
Veja com que elegância agora você pode exibir a tabela inteira:
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
A propósito, exibir e trabalhar com uma tabela pode ser encapsulado em qualquer Sheet
classe separada.
Para continuar ...
Na próxima parte deste artigo, descreverei como usar metaclasses para depurar o código do aplicativo, como parametrizar a criação de uma metaclasse e mostrarei exemplos básicos do uso do método __prepare__
. Fique atento!
Mais detalhadamente sobre metaclasses e descritores em Python, contarei na estrutura do Advanced Python .