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 ( MetaClassno nosso caso).
- O método MetaClass.__prepare__éMetaClass.__prepare__- ele deve retornar um objeto semelhante aoMetaClass.__prepare__no qual os atributos e métodos da classe serão gravados. Depois disso, o objeto será passado para oMetaClass.__new__através do argumentoattrs. Falaremos sobre o uso prático desse método um pouco mais adiante nos exemplos.
- O intérprete lê o corpo da classe Usere gera parâmetros para transmiti-los à metaclasseMetaClass.
- O método MetaClass.__new__éMetaClass.__new__- o métodoMetaClass.__new__, retorna o objeto de classe criado. Já conhecemos os argumentosname,baseseattrsquando os passamos para a funçãotype, e falaremos sobre o parâmetro**extra_kwargsum pouco mais tarde. Se o tipo do argumentoattrsfoi alterado usando__prepare__, ele deve ser convertido em umdictantes de passá-lo para a chamada do métodosuper().
- 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 objetoattrs.
- 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 oMetaClass.__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__chamaUser.__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 Rownão possui atributos de classe com os nomesnameeage- são anotações de tipo , portanto não estão nas chaves do dicionárioattrse, para obter uma lista de campos, usamos o__annotations__classe__annotations__.
- A operação cls.row_count += 1deveria enganar você: como assim? Afinal,clsé uma classeRow; não possui o atributorow_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 .