Skip to content

Latest commit

 

History

History
1473 lines (1140 loc) · 99.1 KB

cap24.adoc

File metadata and controls

1473 lines (1140 loc) · 99.1 KB

Metaprogramação de classes

Todo mundo sabe que depurar um programa é duas vezes mais difícil que escrever o mesmo programa. Mas daí, se você der tudo de si ao escrever o programa, como vai conseguir depurá-lo?[1]

— Brian W. Kernighan and P. J. Plauger
The Elements of Programming Style

Metaprogramação de classes é a arte de criar ou personalizar classes durante a execução do programa. Em Python, classes são objetos de primeira classe, então uma função pode ser usada para criar uma nova classe a qualquer momento, sem usar a palavra-chave class.

Decoradores de classes também são funções, mas são projetados para inspecionar, modificar ou mesmo substituir a classe decorada por outra classe. Por fim, metaclasses são a ferramenta mais avançada para metaprogramação de classes: elas permitem a criação de categorias de classes inteiramente novas, com características especiais, tais como as classes base abstratas, que já vimos anteriormente.

Metaclasses são poderosas, mas difíceis de justificar na prática, e ainda mais difíceis de entender direito. Decoradores de classe resolvem muitos dos mesmos problemas, e são mais fáceis de compreender. Mais ainda, o Python 3.6 implementou a PEP 487—Simpler customization of class creation (PEP 487—Uma personalização mais simples da criação de classes), fornecendo métodos especiais para tarefas que antes exigiam metaclasses ou decoradores de classe.[2]

Este capítulo apresenta as técnicas de metaprogramação de classes em ordem ascendente de complexidade.

Warning

Esse é um tópico empolgante, e é fácil se deixar levar pelo entusiasmo. Então preciso deixar aqui esse conselho.

Em nome da legibilidade e facilidade de manutenção, você provavelmente deveria evitar as técnicas descritas neste capítulo em aplicações.

Por outro lado, caso você queira escrever o próximo framework formidável do Python, essas são suas ferramentas de trabalho.

Novidades nesse capítulo

Todo o código do capítulo "Metaprogramação de Classes" da primeira edição do Python Fluente ainda funciona corretamente. Entretanto, alguns dos exemplos antigos não representam mais as soluções mais simples, tendo em vista os novos recursos surgidos desde o Python 3.6.

Substituí aqueles exemplos por outros, enfatizando os novos recursos de metaprogramação ou acrescentando novos requisitos para justificar o uso de técnicas mais avançadas. Alguns destes novos exemplos se valem de dicas de tipo para fornecer fábricas de classes similares ao decorador @dataclass e a typing.NamedTuple.

A Metaclasses no mundo real é nova, trazendo algumas considerações de alto nível sobre a aplicabilidade das metaclasses.

Tip

Algumas das melhores refatorações envolvem a remoção de código tornado redundante por formas novas e e mais simples de resolver o mesmo problema. Isso se aplica tanto a código em produção quando a livros.

Vamos começar revisando os atributos e métodos definidos no Modelo de Dados do Python para todas as classes.

Classes como objetos

Como acontece com a maioria das entidades programáticas do Python, classes também são objetos. Toda classe tem alguns atributos definidos no Modelo de Dados do Python, documentados na seção "4.13. Atributos Especiais" do capítulo "Tipos Embutidos" da Biblioteca Padrão do Python. Três destes atributos já apareceram várias vezes no livro: __class__, __name__, and __mro__. Outros atributos de classe padrão são:

cls.__bases__

A tupla de classes base da classe.

cls.__qualname__

O nome qualificado de uma classe ou função, que é um caminho pontuado, desde o escopo global do módulo até a definição da classe. Isso é relevante quando a classe é definida dentro de outra classe. Por exemplo, em um modelo de classe Django, tal como Ox (EN), há uma classe interna chamada Meta. O __qualname__ de Meta é Ox.Meta, mas seu __name__ é apenas Meta. A especificação para este atributo está na PEP 3155—​Qualified name for classes and functions (PEP 3155—Nome qualificado para classes e funções) (EN).

cls.__subclasses__()

Este método devolve uma lista das subclasses imediatas da classe. A implementação usa referências fracas, para evitar referências circulares entre a superclasse e suas subclasses—que mantêm uma referência forte para a superclasse em seus atributos __bases__. O método lista as subclasses na memória naquele momento. Subclasses em módulos ainda não importados não aparecerão no resultado.

cls.mro()

O interpretador invoca este método quando está criando uma classe, para obter a tupla de superclasses armazenada no atributo __mro__ da classe. Uma metaclasse pode sobrepor este método, para personalziar a ordem de resolução de métodos da classe em construção.

Tip

Nenhum dos atributos mencionados nesta seção aparecem na lista devolvida pela função dir(…).

Agora, se classe é um objeto, o que é a classe de uma classe?

type: a fábrica de classes embutida

Nós normalmente pensamos em type como uma função que devolve a classe de um objeto, porque é isso que type(my_object) faz: devolve my_object.class.

Entretanto, type é uma classe que cria uma nova classe quando invocada com três argumentos.

Considere essa classe simples:

class MyClass(MySuperClass, MyMixin):
    x = 42

    def x2(self):
        return self.x * 2

Usando o construtor type, podemos criar MyClass durante a execução, com o seguinte código:

MyClass = type('MyClass',
               (MySuperClass, MyMixin),
               {'x': 42, 'x2': lambda self: self.x * 2},
          )

Aquela chamada a type é funcionalmente equivalente ao bloco sob a instrução class MyClass… anterior.

Quando o Python lê uma instrução class, invoca type para construir um objeto classe com os parâmetros abaixo:

name

O identificador que aparece após a palavra-chave class, por exemplo, MyClass.

bases

A tupla de superclasses passadas entre parênteses após o identificador da classe, ou (object,), caso nenhuma superclasse seja mencionada na instrução class.

dict

Um mapeamento entre nomes de atributo e valores. Invocáveis se tornam métodos, como vimos na [methods_are_descriptors_sec]. Outros valores se tornam atributos de classe.

Note

O construtor type aceita argumentos nomeados opcionais, que são ignorados por type, mas que são passados como recebidos para __init_subclass__, que deve consumi-los. Vamos estudar esse método especial na Apresentando __init_subclass__, mas não vou tratar do uso de argumentos nomeados. Para saber mais sobre isso, por favor leia a PEP 487—Simpler customization of class creation (PEP 487—Uma personalização mais simples da criação de classes) (EN).

A classe type é uma metaclasse: uma classe que cria classes. Em outras palavras, instâncias da classe type são classes. A biblioteca padrão contém algumas outras metaclasses, mas type é a default:

>>> type(7)
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(OSError)
<class 'type'>
>>> class Whatever:
...     pass
...
>>> type(Whatever)
<class 'type'>

Vamos criar metaclasses personalizadas na Introdução às metaclasses.

Agora, vamos usar a classe embutida type para criar uma função que constrói classes.

Uma função fábrica de classes

A biblioteca padrão contém uma função fábrica de classes que já apareceu várias vezes aqui: collections.namedtuple. No [data_class_ch] também vimos typing.NamedTuple e @dataclass. Todas essas fábricas de classe usam técnicas que veremos neste capítulo.

Vamos começar com uma fábrica muito simples, para classes de objetos mutáveis—a substituta mais simples possível de @dataclass.

Suponha que eu esteja escrevendo uma aplicação para uma pet shop, e queira armazenar dados sobre cães como registros simples. Mas não quero escrever código padronizado como esse:

class Dog:
    def __init__(self, name, weight, owner):
        self.name = name
        self.weight = weight
        self.owner = owner

Chato…​ cada nome de campo aparece três vezes, e essa repetição sequer nos garante um bom repr:

>>> rex = Dog('Rex', 30, 'Bob')
>>> rex
<__main__.Dog object at 0x2865bac>

Inspirados por collections.namedtuple, vamos criar uma record_factory, que cria classes simples como Dog em tempo real. O Exemplo 1 mostra como ela deve funcionar.

Exemplo 1. Testando record_factory, uma fábrica de classes simples
link:code/24-class-metaprog/factories.py[role=include]
  1. A fábrica pode ser chamada como namedtuple: nome da classe, seguido dos nomes dos atributos separados por espaços, em um única string.

  2. Um repr agradável.

  3. Instâncias são iteráveis, então elas podem ser convenientemente desempacotadas em uma atribuição…​

  4. …​ou quando são passadas para funções como format.

  5. Uma instância do registro é mutável.

  6. A classe recém-criada herda de object—não tem qualquer relação com nossa fábrica.

O código para record_factory está no Exemplo 2.[3]

Exemplo 2. record_factory.py: uma classe fábrica simples
link:code/24-class-metaprog/factories.py[role=include]
  1. O usuário pode fornecer os nomes dos campos como uma string única ou como um iterável de strings.

  2. Aceita argumentos como os dois primeiros de collections.namedtuple; devolve type—isto é, uma classe que se comporta como uma tuple.

  3. Cria uma tupla de nomes de atributos; esse será o atributo __slots__ da nova classe.

  4. Essa função se tornará o método __init__ na nova classe. Ela aceita argumentos posicionais e/ou nomeados.[4]

  5. Produz os valores dos campos na ordem dada por __slots__.

  6. Produz um repr agradável, iterando sobre __slots__ e self.

  7. Monta um dicionário de atributos de classe.

  8. Cria e devolve a nova classe, invocando o construtor de type.

  9. Converte names separados por espaços ou vírgulas em uma lista de str.

O Exemplo 2 é a primeira vez que vemos type em uma dica de tipo. Se a anotação fosse apenas → type, significaria que record_factory devolve uma classe—e isso estaria correto. Mas a anotação → type[tuple] é mais precisa: indica que a classe devolvida será uma subclasse de tuple.

A última linha de record_factory no Exemplo 2 cria uma classe cujo nome é o valor de cls_name, com object como sua única classe base imediata, e um espaço de nomes carregado com __slots__, __init__, __iter__, e __repr__, sendo os útimos três métodos de instância.

Poderíamos ter dado qualquer outro nome ao atributo de classe __slots__, mas então teríamos que implementar __setattr__ para validar os nomes dos atributos em uma atribuição, porque em nossas classes similares a registros queremos que o conjunto de atributos seja sempre o mesmo e na mesma ordem. Entretanto, lembre-se que a principal característica de __slots__ é economizar memória quando estamos lidando com milhões de instâncias, e que usar __slots__ traz algumas desvantagens, discutidas na [slots_section].

Warning

Instâncias de classes criadas por record_factory não são serializáveis—​isto é, elas não podem ser exportadas com a função dump do módulo pickle. Resolver este problema está além do escopo desse exemplo, que tem por objetivo mostrar a classe type funcionando em um caso de uso simples. Para uma solução completa, estude o código-fonte de collections.namedtuple; procure pela palavra "pickling".

Vamos ver agora como emular fábricas de classes mais modernas, como typing.NamedTuple, que recebe uma classe definida pelo usuário, escrita com o comando class, e a melhora automaticamente, acrescentando funcionalidade.

Apresentando __init_subclass__

Tanto __init_subclass__ quanto __set_name__ foram propostos na PEP 487—Simpler customization of class creation (PEP 487—Uma personalização mais simples da criação de classes). Falamos pela primeira vez do método especial para descritores __set_name__ na [auto_storage_sec]. Agora vamos estudar __init_subclass__.

No [data_class_ch], vimos como typing.NamedTuple e @dataclass permitem a programadores usarem a instrução class para especificar atributos para uma nova classe, que então é aperfeiçoada pela fábrica de classes com a adição automática de métodos essenciais, tais como __init__, __repr__, __eq__, etc.

Ambas as fábricas de classes leem as dicas de tipo na instrução class do usuário para aperfeiçoar a classe. Essas dicas de tipo também permitem que verificadores de tipo estáticos validem código que define ou lê aqueles atributos. Entretanto, NamedTuple e @dataclass não se valem das dicas de tipo para validação de atributos durante a execução. A classe Checked, no próximo exemplo, faz isso.

Note

Não é possível suportar toda dica de tipo estática concebível para verificação de tipo durante a execução, e possivelmente essa é a razão para typing.NamedTuple e @dataclass sequer tentarem. Entretanto, algums tipos que são também classes concretas podem ser usados com Checked. Isso inclui tipos simples, usados com frequência para o conteúdo de campos, tais como str, int, float, e bool, bem como listas destes tipos.

O Exemplo 3 mostra como usar Checked para criar uma classe Movie.

Exemplo 3. initsub/checkedlib.py: doctest para a criação de uma subclasse Movie de Checked
link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]
  1. Movie herda de Checked—que definiremos mais tarde, no Exemplo 5.

  2. Cada atributo é anotado com um construtor. Aqui usei tipos embutidos.

  3. Instâncias de Movie devem ser criadas usando argumentos nomeados.

  4. Em troca, temos um __repr__ agradável.

Os construtores usados como dicas de tipo podem ser qualquer invocável que receba zero ou um argumento, e devolva um valor adequado ao tipo do campo pretendido ou rejeite o argumento, gerando um TypeError ou um ValueError.

Usar tipos embutidos para as anotações no Exemplo 3 significa que os valores devem aceitáveis pelo construtor do tipo. Para int, isso significa qualquer x tal que int(x) devolva um int. Para str, qualquer coisa serve durante a execução, pois str(x) funciona com qualquer x no Python.[5]

Quando chamado sem argumentos, o construtor deve devolver um valor default de seu tipo.[6]

Esse é o comportamento padrão de construtores embutidos no Python:

>>> int(), float(), bool(), str(), list(), dict(), set()
(0, 0.0, False, '', [], {}, set())

Em uma subclasse de Checked como Movie, parâmetros ausentes criam instâncias com os valores default devolvidos pelos construtores dos campos. Por exemplo:

link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]

Os construtores são usados para validação durante a instanciação, e quando um atributo é definido diretamente em uma instância:

link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]
Warning
Subclasses de Checked e a verificação estática de tipos

Em um arquivo de código fonte .py contendo uma instância movie da classe Movie, como definida no Exemplo 3, o Mypy marca essa atribuição como um erro de tipo:

movie.year = 'MCMLXXII'

Entretanto, o Mypy não consegue detectar erros de tipo nessa chamada ao construtor:

blockbuster = Movie(title='Avatar', year='MMIX')

Isso porque Movie herda Checked.__init__, e a assinatura daquele método deve aceitar qualquer argumento nomeado, para suportar classes arbitrárias definidas pelo usuário.

Por outro lado, se você declarar um campo de uma subclasse de Checked com a dica de tipo list[float], o Mypy pode sinalizar atribuições de listas com tipos incompatíveis, mas Checked vai ignorar o parâmetro de tipo e tratá-lo como igual a list.

Vamos ver agora a implementação de checkedlib.py. A primeira classe é o descritor Field, como mostra o Exemplo 4.

Exemplo 4. initsub/checkedlib.py: a classe descritora Field
link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]
  1. Lembre-se, desde o Python 3.9, o tipo Callable para anotações é a ABC em collections.abc, e não o descontinuado typing.Callable.

  2. Essa é a dica de tipo Callable mínima; o parâmetro de tipo e o tipo devolvido para constructor são ambos implicitamente Any.

  3. Para verificação durante a execução, usamos o embutido callable.[7] O teste contra type(None) é necessário porque o Python entende None em um tipo como NoneType, a classe de None (e portanto invocável), mas esse é um construtor inútil, que apenas devolve None.

  4. Se Checked.__init__ definir value como …​ (o objeto embutido Ellipsis), invocamos o construtor sem argumentos.

  5. Caso contrário, invocamos o constructor com o value dado.

  6. Se constructor gerar qualquer dessas exceções, geramos um TypeError com uma mensagem útil, incluindo os nomes do campo e do construtor; por exemplo, 'MMIX' não é compatível com year:int.

  7. Se nenhuma exceção for gerada, o value é armazenado no instance.__dict__.

Em __set__, precisamos capturar TypeError e ValueError, pois os construtores embutidos podem gerar qualquer dos dois, dependendo do argumento. Por exemplo, float(None) gera um TypeError, mas float('A') gera um ValueError. Por outro lado, float('8') não causa qualquer erro, e devolve 8.0. E assim eu aqui declaro que, nesse exemplo simples, este um recurso, não um bug.

Tip

Na [auto_storage_sec], vimos o conveniente método especial __set_name__ para descritores. Não precisamos disso na classe Field, porque os descritores não são instanciados no código-fonte cliente; o usuário declara tipos que são construtores, como visto na classe Movie (no Exemplo 3). Em vez disso, as instâncias do descritor Field são criadas durante a execução, pelo método Checked.__init_subclass__, que veremos no Exemplo 5.

Vamos agora nos concentrar na classe Checked, que dividi em duas listagens. O Exemplo 5 mostra a parte inicial da classe, incluindo os métodos mais importantes para esse exemplo. O restante dos métodos está no Exemplo 6.

Exemplo 5. initsub/checkedlib.py: os métodos mais importante da classe Checked
link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]
  1. Escrevi este método de classe para ocultar a chamada a typing.get_type_hints do resto da classe. Se precisasse suportar apenas versões do Python ≥ 3.10, invocaria inspect.get_annotations em vez disso. Reveja a [problems_annot_runtime_sec] para uma discussão dos problemas com essas funções.

  2. __init_subclass__ é chamado quando uma subclasse da classe atual é definida. Ele recebe aquela nova subclasse como seu primeiro argumento—e por isso nomeei o argumento subclass em vez do habitual cls. Para mais informações sobre isso, veja __init_subclass__ não é um método de classe típico.

  3. super().__init_subclass__() não é estritamente necessário, mas deve ser invocado para ajudar outras classes que implementem .__init_subclass__() na mesma árvore de herança. Veja a [mro_section].

  4. Itera sobre name e constructor em cada campo…​

  5. …​criando um atributo em subclass com aquele name vinculado a um descritor Field, parametrizado com name e constructor.

  6. Para cada name nos campos da classe…​

  7. …​obtém o value correspondente de kwargs e o remove de kwargs. Usar …​ (o objeto Ellipsis) como default nos permite distinguir entre argumentos com valor None de argumentos ausentes.[8]

  8. Essa chamada a setattr aciona Checked.__setattr__, apresentado no Exemplo 6.

  9. Se houver itens remanescentes em kwargs, seus nomes não correspondem a qualquer dos campos declarados, e __init__ vai falhar.

  10. Esse erro é informado por __flag_unknown_attrs, listado no Exemplo 6. Ele recebe um argumento *names com os nomes de atributos desconhecidos. Usei um único asterisco em *kwargs, para passar suas chaves como uma sequência de argumentos.

__init_subclass__ não é um método de classe típico

O decorador @classmethod nunca é usado com __init_subclass__, mas isso não quer dizer muita coisa, pois o método especial __new__ se comporta como um método de classe mesmo sem @classmethod. O primeiro argumento que o Python passa para __init_subclass__ é uma classe. Entretanto, essa nunca é a classe onde __init_subclass__ é implementado, mas sim uma subclasse recém-definida daquela classe. Isso é diferente de __new__ e de qualquer outro método de classe que eu conheço. Assim, acho que __init_subclass__ não é um método de classe no sentido usual, e é errado nomear seu primeiro argumento cls. A documentação de __init_suclass__ chama o argumento de cls, mas explica: "…​chamado sempre que se cria uma subclasse da classe que o contém. cls é então a nova subclasse…​"[9].

Vamos examinar os métodos restantes da classe Checked, continuando do Exemplo 5. Observe que prefixei os nomes dos métodos _fields e _asdict com _, pela mesma razão pela qual isso é feito na API de collections.namedtuple: reduzir a possibilidade de colisões de nomes com nomes de campos definidos pelo usuário.

Exemplo 6. initsub/checkedlib.py: métodos restantes da classe Checked
link:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]
  1. Intercepta qualquer tentativa de definir um atributo de instância. Isso é necessário para evitar a definição de um atributo desconhecido.

  2. Se o name do atributo é conhecido, busca o descriptor correspondente.

  3. Normalmente não é preciso invocar o __set__ do descritor explicitamente. Nesse caso isso foi necessário porque __setattr__ intercepta todas as tentativas de definir um atributo em uma instância, mesmo na presença de um descritor dominante, tal como Field.[10]

  4. Caso contrário, o atributo name é desconhecido, e uma exceção será gerada por __flag_unknown_attrs.

  5. Cria uma mensagem de erro útil, listando todos os argumentos inesperados, e gera um AttributeError. Este é um raro exemplo do tipo especial NoReturn, tratado na [noreturn_sec].

  6. Cria um dict a partir dos atributos de um objeto Movie. Eu chamaria este método de _as_dict, mas segui a convenção iniciada com o método _asdict em collections.namedtuple.

  7. Implementar um __repr__ agradável é a principal razão para ter _asdict neste exemplo.

O exemplo Checked mostra como tratar descritores dominantes ao implementar __setattr__ para bloquear a definição arbitrária de atributos após a instanciação. É possível debater se vale a pena implementar __setattr__ neste exemplo. Sem ele, definir movie.director = 'Greta Gerwig' funcionaria, mas o atributo director não seria verificado de forma alguma, não apareceria no __repr__ nem seria incluído no dict devolvido por _asdict—ambos definidos no Exemplo 6.

Em record_factory.py (no Exemplo 2), solucionei essa questão usando o atributo de classe __slots__. Entretanto, essa solução mais simples não é viável aqui, como explicado a seguir.

Por que __init_subclass__ não pode configurar __slots__?

O atributo __slots__ só é efetivo se for um dos elementos do espaço de nomes da classe passado para type.__new__. Acrescentar __slots__ a uma classe existente não tem qualquer efeito. O Python invoca __init_subclass__ apenas após a classe ser criada—neste ponto, é tarde demais para configurar __slots__. Um decorador de classes também não pode configurar __slots__, pois ele é aplicado ainda mais tarde que __init_subclass__. Vamos explorar essas questões de sincronia na O que acontece quando: importação versus execução.

Para configurar __slots__ durante a execução, nosso próprio código precisa criar o espaço de nomes da classe a ser passado como último argumento de type.__new__. Para fazer isso, podemos escrever uma função fábrica de classes, como record_factory.py, ou optar pelo caminho bombástico, e implementar uma metaclasse. Veremos como configurar __slots__ dinamicamente na Introdução às metaclasses.

Antes da PEP 487 (EN) simplificar a personalização da criação de classes com __init_subclass__, no Python 3.7, uma funcionalidade similar só poderia ser implementada usando um decorador de classe. É o tópico de nossa próxima seção.

Melhorando classes com um decorador de classes

Um decorador de classes é um invocável que se comporta de forma similar a um decorador de funções: recebe uma classe decorada como argumento, e deve devolver um classe para substituir a classe decorada. Decoradores de classe frequentemente devolvem a própria classe decorada, após injetar nela mais métodos pela definição de atributos. Provavelmente, a razão mais comum para escolher um decorador de classes, em vez do mais simples __init_subclass__, é evitar interferência com outros recursos da classe, tais como herança e metaclasses.[11]

Nessa seção vamos estudar checkeddeco.py, que oferece a mesma funcionalidade de checkedlib.py, mas usando um decorador de classe. Como sempre, começamos examinando um exemplo de uso, extraído dos doctests em checkeddeco.py (no Exemplo 7).

Exemplo 7. checkeddeco.py: criando uma classe Movie decorada com @checked
link:code/24-class-metaprog/checked/decorator/checkeddeco.py[role=include]

A única diferença entre o Exemplo 7 e o Exemplo 3 é a forma como a classe Movie é declarada: ela é decorada com @checked em vez de ser uma subclasse de Checked. Fora isso, o comportamento externo é o mesmo, incluindo a validação de tipo e a atribuição de valores default, apresentados após o Exemplo 3, na Apresentando __init_subclass__.

Vamos olhar agora para a implementação de checkeddeco.py. As importações e a classe Field são as mesmas de checkedlib.py, listadas no Exemplo 4. Em checkeddeco.py não há qualquer outra classe, apenas funções.

A lógica antes implementada em __init_subclass__ agora é parte da função checked—o decorador de classes listado no Exemplo 8.

Exemplo 8. checkeddeco.py: o decorador de classes
link:code/24-class-metaprog/checked/decorator/checkeddeco.py[role=include]
  1. Lembre-se que classes são instâncias de type. Essas dicas de tipo sugerem fortemente que este é um decorador de classes: ele recebe uma classe e devolve uma classe.

  2. _fields agora é uma função de alto nível definida mais tarde no módulo (no Exemplo 9).

  3. Substituir cada atributo devolvido por _fields por uma instância do descritor Field é o que __init_subclass__ fazia no Exemplo 5. Aqui há mais trabalho a ser feito…​

  4. Cria um método de classe a partir de _fields, e o adiciona à classe decorada. O comentário type: ignore é necessário, porque o Mypy reclama que type não tem um atributo _fields.

  5. Funções ao nível do módulo, que se tornarão métodos de instância da classe decorada.

  6. Adiciona cada um dos instance_methods a cls.

  7. Devolve a cls decorada, cumprindo o contrato básico de um decorador de classes.

Todas as funções no primeiro nível de checkeddeco.py estão prefixadas com um sublinhado, exceto o decorador checked. Essa convenção para a nomenclatura faz sentido por duas razões:

  • checked é parte da interface pública do módulo checkeddeco.py, as outras funções não.

  • As funções no Exemplo 9 serão injetadas na classe decorada, e o _ inicial reduz as chances de um conflito de nomes com atributos e métodos definidos pelo usuário na classe decorada.

O restante de checkeddeco.py está listado no Exemplo 9. Aquelas funções no nível do módulo contém o mesmo código dos métodos correspondentes na classe Checked de checkedlib.py. Elas foram explicadas no Exemplo 5 e no Exemplo 6.

Observe que a função _fields exerce dois papéis em checkeddeco.py. Ela é usada como uma função regular na primeira linha do decorador checked e será também injetada como um método de classe na classe decorada.

Exemplo 9. checkeddeco.py: os métodos que serão injetados na classe decorada
link:code/24-class-metaprog/checked/decorator/checkeddeco.py[role=include]

O módulo checkeddeco.py implementa um decorador de classes simples mas usável. O @dataclass do Python faz muito mais. Ele suporta várias opções de configuração, acrescenta mais métodos à classe decorada, trata ou avisa sobre conflitos com métodos definidos pelo usuário na classe decorada, e até percorre o __mro__ para coletar atributos definidos pelo usuário declarados em superclasses da classe decorada. O código-fonte do pacote dataclasses no Python 3.9 tem mais de 1200 linhas.

Para fazer metaprogramação de classes, precisamos saber quando o interpretador Python avalia cada bloco de código durante a criação de uma classe. É disso que falaremos a seguir.

O que acontece quando: importação versus execução

Programadores Python falam de "importação" (import time) versus "execução" (runtime), mas estes termos não tem definições precisas e há uma zona cinzenta entre eles.

Na importação, o interpretador:

  1. Analisa o código-fonte de módulo .py em uma passagem, de cima até embaixo. É aqui que um SyntaxError pode ocorrer.

  2. Compila o bytecode a ser executado.

  3. Executa o código no nível superior do módulo compilado.

Se existir um arquivo .pyc atualizado no __pycache__ local, a análise e a compilação são omitidas, pois o bytecode está pronto para ser executado.

Apesar da análise e a compilação serem definitivamente atividades de "importação", outras coisas podem acontecer durante o processo, pois quase todos os comandos ou instruções no Python são executáveis, no sentido de poderem potencialmente rodar código do usuário e modificar o estado do programa do usuário.

Em especial, a instrução import não é meramente uma declaração[12], pois na verdade ela executa todo o código no nível superior de um módulo, quando este é importado para o processo pela primeira vez. Importações posteriores do mesmo módulo usarão um cache, e então o único efeito será a vinculação dos objetos importados a nomes no módulo cliente. Aquele código no primeiro nível pode fazer qualquer coisa, incluindo ações típicas da "execução", tais como escrever em um arquivo de log ou conectar-se a um banco de dados.[13] Por isso a fronteira entre a "importação" e a "execução" é difusa: import pode acionar todo tipo de comportamento de "execução", porque a instrução import e a função embutida __import__() podem ser usadas dentro de qualquer função regular.

Tudo isso é bastante abstrato e sútil, então vamos realizar alguns experimentos para ver o que acontece, e quando.

Experimentos com a fase de avaliação (evaluation time)

Considere um script evaldemo.py, que usa um decorador de classes, um descritor e uma fábrica de classes baseada em __init_subclass__, todos definidos em um módulo builderlib.py. Os módulos usados tem várias chamadas a print, para revelar o que acontece por baixo dos panos. Fora isso, eles não fazem nada de útil. O objetivo destes experimentos é observar a ordem na qual essas chamadas a print acontecem.

Warning

Aplicar um decorador de classes e uma fábrica de classes com __init_subclass__ juntos, em uma única classe, é provavelmente um sinal de excesso de engenharia ou de desespero. Essa combinação incomum é útil nesses experimentos, para mostrar a ordem temporal das mudanças que um decorador de classes e __init_subclass__ podem aplicar a uma classe.

Vamos começar examinando builderlib.py, dividido em duas partes: o Exemplo 10 e o Exemplo 11.

Exemplo 10. builderlib.py: primeira parte do módulo
link:code/24-class-metaprog/evaltime/builderlib.py[role=include]
  1. Essa é uma fábrica de classes para implementar…​

  2. …​um método __init_subclass__.

  3. Define uma função para ser adicionada à subclasse na atribuição abaixo.

  4. Um decorador de classes.

  5. Função a ser adicionada à classe decorada.

  6. Devolve a classe recebida como argumento.

Continuando builderlib.py no Exemplo 11…​

Exemplo 11. builderlib.py: a parte final do módulo
link:code/24-class-metaprog/evaltime/builderlib.py[role=include]
  1. Uma classe descritora para demonstrar quando…​

  2. …​uma instância do descritor é criada, e quando…​

  3. …​__set_name__ será invocado durante a criação da classe owner.

  4. Como os outros métodos, este __set__ não faz nada, exceto exibir seus argumentos.

Se importarmos builderlib.py no console do Python, veremos o seguinte:

>>> import builderlib
@ builderlib module start
@ Builder body
@ Descriptor body
@ builderlib module end

Observe que as linhas exibidas por builderlib.py tem um @ como prefixo.

Vamos agora voltar a atenção para evaldemo.py, que vai acionar método especiais em builderlib.py (no Exemplo 12).

Exemplo 12. evaldemo.py: script para experimentar com builderlib.py
link:code/24-class-metaprog/evaltime/evaldemo.py[role=include]
  1. Aplica um decorador.

  2. Cria uma subclasse de Builder para acionar seu __init_subclass__.

  3. Instancia o descritor.

  4. Isso só será chamado se o módulo for executado como o programa pincipal.

As chamadas a print em evaldemo.py tem um # como prefixo. Se você abrir o console novamente e importar evaldemo.py, a saída aparece no Exemplo 13.

Exemplo 13. Experimentos de console com evaldemo.py
>>> import evaldemo
@ builderlib module start  (1)
@ Builder body
@ Descriptor body
@ builderlib module end
# evaldemo module start
# Klass body  (2)
@ Descriptor.__init__(<Descriptor instance>)  (3)
@ Descriptor.__set_name__(<Descriptor instance>,
      <class 'evaldemo.Klass'>, 'attr')                (4)
@ Builder.__init_subclass__(<class 'evaldemo.Klass'>)  (5)
@ deco(<class 'evaldemo.Klass'>)  (6)
# evaldemo module end
  1. As primeiras quatro linhas são o resultado de from builderlib import…. Elas não vão aparecer se você não fechar o console após o experimento anterior, pois builderlib.py já estará carregado.

  2. Isso sinaliza que o Python começou a ler o corpo de Klass. Neste momento o objeto classe ainda não existe.

  3. A instância do descritor é criada e vinculada a attr, no espaço de nomes que o Python passará para o construtor default do objeto classe: type.__new__.

  4. Neste ponto, a função embutida do Python type.__new__ já criou o objeto Klass e invoca __set_name__ em cada instância das classes do descritor que oferecem aquele método, passando Klass como argumento owner.

  5. type.__new__ então chama __init_subclass__ na superclasse de Klass, passando Klass como único argumento.

  6. Quando type.__new__ devolve o objeto classe, o Python aplica o decorador. Neste exemplo, a classe devolvida por deco está vinculada a Klass no espaço de nomes do módulo

A implementação de type.__new__ está escrita em C. O comportamento que acabei de descrever está documentado na seção "Criando o objeto classe", no capítulo "Modelo de Dados" da referência do Python.

Observe que a função main() de evaldemo.py (no Exemplo 12) não foi executada durante a sessão no console (no Exemplo 13), portanto nenhuma instância de Klass foi criada. Todas as ações que vimos foram acionadas por operações de "importação": importar builderlib e definir Klass.

Se você executar evaldemo.py como um script, vai ver a mesma saída do Exemplo 13, com linhas extras logo antes do final. As linhas adicionais são o resultado da execução de main() (veja o Exemplo 14).

Exemplo 14. Executando evaldemo.py como um programa
$ ./evaldemo.py
[... 9 linhas omitidas ...]
@ deco(<class '__main__.Klass'>)  (1)
@ Builder.__init__(<Klass instance>)  (2)
# Klass.__init__(<Klass instance>)
@ SuperA.__init_subclass__:inner_0(<Klass instance>)  (3)
@ deco:inner_1(<Klass instance>)  (4)
@ Descriptor.__set__(<Descriptor instance>, <Klass instance>, 999)  (5)
# evaldemo module end
  1. As 10 primeiras linhas—incluindo essa—são as mesma que aparecem no Exemplo 13.

  2. Acionado por super().__init__() em Klass.__init__.

  3. Acionado por obj.method_a() em main; o method_a foi injetado por SuperA.__init_subclass__.

  4. Acionado por obj.method_b() em main; method_b foi injetado por deco.

  5. Acionado por obj.attr = 999 em main.

Uma classe base com __init_subclass__ ou um decorador de classes são ferramentas poderosas, mas elas estão limitadas a trabalhar sobre uma classe já criada por type.__new__ por baixo dos panos. Nas raras ocasiões em que for preciso ajustar os argumentos passados a type.__new__, uma metaclasse é necessária. Esse é o destino final desse capítulo—e desse livro.

Introdução às metaclasses

[Metaclasses] são uma mágica tão profunda que 99% dos usuários jamais deveria se preocupar com elas. Quem se pergunta se precisa delas, não precisa (quem realmente precisa de metaclasses sabe disso com certeza, e não precisa que lhe expliquem a razão).[14]

— Tim Peters
inventor do algoritmo timsort e um produtivo colaborador do Python

Uma metaclasse é uma fábrica de classes. Diferente de record_factory, do Exemplo 2, uma metaclasse é escrita como uma classe. Em outras palavras, uma metaclasse é uma classe cujas instâncias são classes. A Figura 1 usa a Notação Engenhocas e Bugigangas para representar uma metaclasse: uma engenhoca que produz outra engenhoca.

Diagrama MGN com metaclasse e classe.
Figura 1. Uma metaclasse é uma classe que cria classes.

Pense no modelo de objetos do Python: classes são objetos, portanto cada classe deve ser uma instância de alguma outra classe. Por default, as classes do Python são instâncias de type. Em outras palavras, type é a metaclasse da maioria das classes, sejam elas embutidas ou definidas pelo usuário:

>>> str.__class__
<class 'type'>
>>> from bulkfood_v5 import LineItem
>>> LineItem.__class__
<class 'type'>
>>> type.__class__
<class 'type'>

Para evitar regressões infinitas, a classe de type é type, como mostra a última linha.

Observe que não estou dizendo que str ou LineItem são subclasses de type. Estou dizendo que str e LineItem são instâncias de type. Elas são todas subclasses de object. A Figura 2 pode ajudar você a contemplar essa estranha realidade.

Diagrama de classes UML com as relações de `object` e `type`.
Figura 2. Os dois diagramas são verdadeiros. O da esquerda enfatiza que str, type, e LineItem são subclasses de object. O da direita deixa claro que str, object, e LineItem são instâncias de type, pois todas são classes.
Note

As classes object e type tem uma relação singular: object é uma instância de type, e type é uma subclasse de object. Essa relação é "mágica": ela não pode ser expressa em Python, porque cada uma das classes teria que existir antes da outra poder ser definida. O fato de type ser uma instância de si mesma também é mágico.

O próximo trecho mostra que a classe de collections.Iterable é abc.ABCMeta. Observe que Iterable é uma classe abstrata, mas ABCMeta é uma classe concreta—​afinal, Iterable é uma instância de ABCMeta:

>>> from collections.abc import Iterable
>>> Iterable.__class__
<class 'abc.ABCMeta'>
>>> import abc
>>> from abc import ABCMeta
>>> ABCMeta.__class__
<class 'type'>

Por fim, a classe de ABCMeta também é type. Toda classe é uma instância de type, direta ou indiretamente, mas apenas metaclasses são também subclasses de type. Essa é a mais importante relação para entender as metaclasses: uma metaclasse, tal como ABCMeta, herda de type o poder de criar classes. A Figura 3 ilustra essa relação fundamental.

Diagramas de classe UML com as relações de `Iterable` e `ABCMeta`.
Figura 3. Iterable é uma subclasse de object e uma instância de ABCMeta. Tanto object quanto ABCMeta são instâncias de type, mas a relação crucial aqui é que ABCMeta também é uma subclasse de type, porque ABCMeta é uma metaclasse. Neste diagrama, Iterable é a única classe abstrata.

A lição importante aqui é que metaclasses são subclasses de type, e é isso que permite a elas funcionarem como fábricas de classes. Uma metaclasse pode personalizar suas instâncias implementando métodos especiais, como demosntram as próximas seções.

Como uma metaclasse personaliza uma classe

Para usar uma metaclasse, é crucial entender como __new__ funciona em qualquer classe. Isso foi discutido na [flexible_new_sec].

A mesma mecânica se repete no nível "meta", quando uma metaclasse está prestes a criar uma nova instância, que é uma classe. Considere a declaração abaixo:

class Klass(SuperKlass, metaclass=MetaKlass):
    x = 42
    def __init__(self, y):
        self.y = y

Para processar essa instrução class, o Python invoca MetaKlass.__new__ com os seguintes argumentos:

meta_cls

A própria metaclasse(MetaKlass), porque __new__ funciona como um método de classe.

cls_name

A string Klass.

bases

A tupla com um único elemento (SuperKlass,) (ou com mais elementos, em caso de herança múltipla).

cls_dict

Um mapeamento como esse:

{x: 42, `+__init__+`: <function __init__ at 0x1009c4040>}

Ao implementar MetaKlass.__new__, podemos inspecionar e modificar aqueles argumentos antes de passá-los para super().__new__, que por fim invocará type.__new__ para criar o novo objeto classe.

Após super().__new__ retornar, podemos também aplicar processamento adicional à classe recém-criada, antes de devolvê-la para o Python. O Python então invoca SuperKlass.__init_subclass__, passando a classe que criamos, e então aplicando um decorador de classe, se algum estiver presente. Finalmente, o Python vincula o objeto classe a seu nome no espaço de nomes circundante—normalmente o espaço de nomes global do módulo, se a instrução class foi uma instrução no primeiro nível.

O processamento mais comum realizado no __new__ de uma metaclasse é adicionar ou substituir itens no cls_dict—o mapeamento que representa o espaço de nomes da classe em construção. Por exemplo, antes de chamar super().__new__, podemos injetar métodos na classe em construção adicionando funções a cls_dict. Entretanto, observe que adicionar métodos pode também ser feito após a classe ser criada, e é por essa razão que podemos fazer isso usando __init_subclass__ ou um decorador de classe.

Um atributo que precisa ser adicionado a cls_dict antes de se executar type.__new__ é __slots__, como discutido na Por que __init_subclass__ não pode configurar __slots__?. O método __new__ de uma metaclasse é o lugar ideal para configurar __slots__. A próxima seção mostra como fazer isso.

Um belo exemplo de metaclasse

A metaclasse MetaBunch, apresentada aqui, é uma variação do último exemplo no Capítulo 4 do Python in a Nutshell, 3ª ed., de Alex Martelli, Anna Ravenscroft, e Steve Holden, escrito para rodar sob Python 2.7 e 3.5.[15] Assumindo o uso do Python 3.6 ou mais recente, pude simplificar ainda mais o código.

Mas primeiro vamos ver o que a classe base Bunch oferece:

link:code/24-class-metaprog/metabunch/from3.6/bunch.py[role=include]

Lembre-se que Checked atribui nomes aos descritores Field em subclasses, baseada em dicas de tipo de variáveis de classe, que não se tornam atributos na classe, já que não tem valores.

Subclasses de Bunch, por outro lado, usam atributos de classe reais com valores, que então se tornam os valores default dos atributos de instância. O __repr__ gerado omite os argumentos para atributos iguais aos defaults.

MetaBunch—a metaclasse de Bunch—gera __slots__ para a nova classe a partir de atributos de classe declarados na classe do usuário. Isso bloqueia a instanciação e posterior atribuição a atributos não declarados:

link:code/24-class-metaprog/metabunch/from3.6/bunch.py[role=include]

Vamos agora mergulhar no elegante código de MetaBunch, no Exemplo 15.

Exemplo 15. metabunch/from3.6/bunch.py: a metaclasse MetaBunch e a classe Bunch
link:code/24-class-metaprog/metabunch/from3.6/bunch.py[role=include]
  1. Para criar uma nova metaclasse, herdamos de type.

  2. __new__ funciona como um método de classe, mas a classe é uma metaclasse, então gosto de nomear o primeiro argumento meta_cls (mcs é uma alternativa comum). Os três argumentos restantes são os mesmos da assinatura de três argumentos de type(), quando chamada diretamente para criar uma classe.

  3. defaults vai manter um mapeamento de nomes de atributos e seus valores default.

  4. Isso irá ser injetado na nova classe.

  5. defaults e define o atributo de instância correspondente, com o valor extraído de kwargs, ou um valor default.

  6. Se ainda houver itens em kwargs, isso significa que não há posição restante onde possamos colocá-los. Acreditamos em falhar rápido como melhor prática, então não queremos ignorar silenciosamente os itens em excesso. Uma solução rápida e eficiente é extrair um item de kwargs e tentar defini-lo na instância, gerando propositalmente um AttributeError.

  7. __repr__ devolve uma string que se parece com uma chamada ao construtor—por exemplo, Point(x=3), omitindo os argumentos nomeados com valores default.

  8. Inicializa o espaço de nomes para a nova classe.

  9. Itera sobre o espaço de nomes da classe do usuário.

  10. Se um name dunder (com sublinhados como prefixo e sufixo) é encontrado, copia o item para o espaço de nomes da nova classe, a menos que ele já esteja lá. Isso evita que usuários sobrescrevam __init__, __repr__ e outros atributos definidos pelo Python, tais como __qualname__ e __module__.

  11. Se name não for um dunder, acrescenta name a __slots__ e armazena seu value em defaults.

  12. Cria e devolve a nova classe.

  13. Fornece uma classe base, assim os usuários não precisam ver MetaBunch.

MetaBunch funciona por ser capaz de configurar __slots__ antes de invocar super().__new__ para criar a classe final. Como sempre em metaprogramação, o fundamental é entender a sequência de ações. Vamos fazer outro experimento sobre a fase de avaliação, agora com uma metaclasse.

Experimento com a fase de avaliação de metaclasses

Essa é uma variação do Experimentos com a fase de avaliação (evaluation time), acrescentando uma metaclasse à mistura. O módulo builderlib.py é o mesmo de antes, mas o script principal é agora evaldemo_meta.py, listado no Exemplo 16.

Exemplo 16. evaldemo_meta.py: experimentando com uma metaclasse
link:code/24-class-metaprog/evaltime/evaldemo_meta.py[role=include]
  1. Importa MetaKlass de metalib.py, que veremos no Exemplo 18.

  2. Declara Klass como uma subclasse de Builder e uma instância de MetaKlass.

  3. Este método é injetado por MetaKlass.__new__, como veremos adiante.

Warning

Em nome da ciência, o Exemplo 16 desafia qualquer racionalidade e aplica três técnicas diferentes de metaprogramação juntas a Klass: um decorador, uma classe base usando __init_subclass__, e uma metaclasse personalizada. Se você fizer isso com código em produção, por favor não me culpe. Repito, o objetivo é observar a ordem na qual as três técnicas interferem no processo de criação de uma classe.

Como no experimento anterior com a fase de avaliação, este exemplo não faz nada, apenas exibe mensagens revelando o fluxo de execução. O Exemplo 17 mostra a primeira parte do código de metalib.py—o restante está no Exemplo 18.

Exemplo 17. metalib.py: a classe NosyDict
link:code/24-class-metaprog/evaltime/metalib.py[role=include]

Escrevi a classe NosyDict para sobrepor __setitem__ e exibir cada key e cada value conforme eles são definidos. A metaclasse vai usar uma instância de NosyDict para manter o espaço de nomes da classe em construção, revelando um pouco mais sobre o funcionamento interno do Python.

A principal atração de metalib.py é a metaclasse no Exemplo 18. Ela implementa o método especial __prepare__, um método de classe que o Python só invoca em metaclasses. O método __prepare__ oferece a primeira oportunidade para influenciar o processo de criação de uma nova classe.

Tip

Ao programar uma metaclasse, acho útil adotar a seguinte convenção de nomenclatura para argumentos de métodos especiais:

  • Usar cls em vez de self para métodos de instância, pois a instância é uma classe.

  • Usar meta_cls em vez de cls para métodos de classe, pois a classe é uma metaclasse. Lembre-se que __new__ se comporta como um método de classe mesmo sem o decorador @classmethod.

Exemplo 18. metalib.py: a MetaKlass
link:code/24-class-metaprog/evaltime/metalib.py[role=include]
  1. __prepare__ deve ser declarado como um método de classe. Ele não é um método de instância, pois a classe em construção ainda não existe quando o Python invoca __prepare__.

  2. O Python invoca __prepare__ em uma metaclasse para obter um mapeamento, onde vai manter o espaço de nomes da classe em construção.

  3. Devolve uma instância de NosyDict para ser usado como o espaço de nomes.

  4. cls_dict é uma instância de NosyDict devolvida por __prepare__.

  5. type.__new__ exige um dict real como último argumento, então passamos a ele o atributo data de NosyDict, herdado de UserDict.

  6. Injeta um método na classe recém-criada.

  7. Como sempre, __new__ precisa devolver o objeto que acaba de ser criado—neste caso, a nova classe.

  8. Definir __repr__ em uma metaclasse permite personalizar o repr() de objetos classe.

O principal caso de uso para __prepare__ antes do Python 3.6 era oferecer um OrderedDict para manter os atributos de uma classe em construção, para que o __new__ da metaclasse pudesse processar aqueles atributos na ordem em que aparecem no código-fonte da definição de classe do usuário. Agora que dict preserva a ordem de inserção, __prepare__ raramente é necessário. Veremos um uso criativo para ele no Um hack de metaclasse com __prepare__.

Importar metalib.py no console do Python não é muito empolgante. Observe o uso de % para prefixar as linhas geradas por esse módulo:

>>> import metalib
% metalib module start
% MetaKlass body
% metalib module end

Muitas coisas acontecem quando importamos evaldemo_meta.py, como visto no Exemplo 19.

Exemplo 19. Experimento com evaldemo_meta.py no console
>>> import evaldemo_meta
@ builderlib module start
@ Builder body
@ Descriptor body
@ builderlib module end
% metalib module start
% MetaKlass body
% metalib module end
# evaldemo_meta module start  (1)
% MetaKlass.__prepare__(<class 'metalib.MetaKlass'>, 'Klass',  (2)
                        (<class 'builderlib.Builder'>,))
% NosyDict.__setitem__(<NosyDict instance>, '__module__', 'evaldemo_meta')  (3)
% NosyDict.__setitem__(<NosyDict instance>, '__qualname__', 'Klass')
# Klass body
@ Descriptor.__init__(<Descriptor instance>)  (4)
% NosyDict.__setitem__(<NosyDict instance>, 'attr', <Descriptor instance>)  (5)
% NosyDict.__setitem__(<NosyDict instance>, '__init__',
                       <function Klass.__init__ at …>)  (6)
% NosyDict.__setitem__(<NosyDict instance>, '__repr__',
                       <function Klass.__repr__ at …>)
% NosyDict.__setitem__(<NosyDict instance>, '__classcell__', <cell at …: empty>)
% MetaKlass.__new__(<class 'metalib.MetaKlass'>, 'Klass',
                    (<class 'builderlib.Builder'>,), <NosyDict instance>)  (7)
@ Descriptor.__set_name__(<Descriptor instance>,
                          <class 'Klass' built by MetaKlass>, 'attr')  (8)
@ Builder.__init_subclass__(<class 'Klass' built by MetaKlass>)
@ deco(<class 'Klass' built by MetaKlass>)
# evaldemo_meta module end
  1. As linhas antes disso são resultado da importação de builderlib.py e metalib.py.

  2. O Python invoca __prepare__ para iniciar o processamento de uma instrução class.

  3. Antes de analisar o corpo da classe, o Python acrescenta __module__ e __qualname__ ao espaço de nomes de uma classe em construção.

  4. A instância do descritor é criada…​

  5. …​e vinculada a attr no espaço de nomes da classe.

  6. Os métodos __init__ e __repr__ são definidos e adicionados ao espaço de nomes.

  7. Após terminar o processamento do corpo da classe, o Python chama MetaKlass.__new__.

  8. __set_name__, __init_subclass__ e o decorador são invocados nessa ordem, após o método __new__ da metaclasse devolver a classe recém-criada.

Se executarmos evaldemo_meta.py como um script, main() é chamado, e algumas outras coisas acontecem (veja o Exemplo 20).

Exemplo 20. Rodando evaldemo_meta.py como um programa
$ ./evaldemo_meta.py
[... 20 linhas omitidas ...]
@ deco(<class 'Klass' built by MetaKlass>)  (1)
@ Builder.__init__(<Klass instance>)
# Klass.__init__(<Klass instance>)
@ SuperA.__init_subclass__:inner_0(<Klass instance>)
@ deco:inner_1(<Klass instance>)
% MetaKlass.__new__:inner_2(<Klass instance>)  (2)
@ Descriptor.__set__(<Descriptor instance>, <Klass instance>, 999)
# evaldemo_meta module end
  1. As primeiras 21 linhas—incluindo esta—são as mesmas que aparecem no Exemplo 19.

  2. Acionado por obj.method_c() em main; method_c foi injetado por MetaKlass.__new__.

Vamos agora voltar à ideia da classe Checked, com descritores Field implementando validação de tipo durante a execução, e ver como aquilo pode ser feito com uma metaclasse.

Uma solução para Checked usando uma metaclasse

Não quero encorajar a otimização prematura nem excessos de engenharia, então aqui temos um cenário de faz de conta para justificar a reescrever checkedlib.py com __slots__, exigindo a aplicação de uma metaclasse. Sinta-se a vontade para pular a historinha.

Uma contação de história

Nosso checkedlib.py usando __init_subclass__ é um sucesso na empresa, e em qualquer dado momento nossos servidores de produção guardam milhões de instâncias de subclasses de Checked em suas memórias.

Analisando o perfil de uma prova de conceito, descobrimos que usar __slots__ pode reduzi os custos de hospedagem, por duas razões:

  • Menos uso de memória, já que as instâncias de Checked não precisarão manter seus próprios __dict__

  • Melhor desempenho, pela remoção de __setattr__, que foi criado só para bloquear atributos inesperados, mas é acionado na instanciação e para todas as definições de atributos antes de Field.__set__ ser chamado para realizar seu trabalho

O módulo metaclass/checkedlib.py, que estudaremos a seguir, é um substituto instantâneo para initsub/checkedlib.py. Os doctests embutidos nos dois módulos são idênticos, bem como os arquivos checkedlib_test.py para o pytest.

A complexidade de checkedlib.py é ocultada do usuário. Aqui está o código-fonte de um script que usa o pacote:

link:code/24-class-metaprog/checked/metaclass/checked_demo.py[role=include]

Essa definição concisa da classe Movie se vale de três instâncias do descritor de validação Field, uma configuração de __slots__, cinco métodos herdados de Checked e uma metaclasse para juntar tudo isso. A única parte visível de checkedlib é a classe base Checked.

Observe a Figura 4. A Notação Engenhocas e Bugigangas complementa o diagrama de classes UML, tornando mais visível a relação entre classes e instâncias.

Por exemplo, uma classe Movie usando a nova checkedlib.py é uma instância de CheckedMeta e uma subclasse de Checked. Adicionalmente, os atributos de classe title, year e box_office de Movie são três instâncias diferentes de Field. Cada instância de Movie tem seus próprios atributos _title, _year e _box_office, para armazenar os valores dos campos correspondentes.

Vamos agora estudar o código, começando pela classe Field, exibida no Exemplo 21.

A classe descritora Field agora está um pouco diferente. Nos exemplos anteriores, cada instância do descritor Field armazenava seu valor na instância gerenciada, usando um atributo de mesmo nome. Por exemplo, na classe Movie, o descritor title armazenava o valor do campo em um atributo title na instância gerenciada. Isso tornava desnecessário que Field implementasse um método __get__.

Entretanto, quando uma classe como Movie usa __slots__, ela não pode ter atributos de classe e atributos de instância com o mesmo nome. Cada instância do descritor é um atributo de classe, e agora precisamos de atributos de armazenamento separados em cada instância. O código usa o nome do descritor prefixado por um único _. Portanto, instâncias de Field têm atributos name e storage_name distintos, e implementamos Field.__get__.

Diagrama de classes UML+MGN para `CheckedMeta`, `Movie` etc.
Figura 4. Diagrama de classes UML com MGN: a meta-engenhoca CheckedMeta cria a engenhoca Movie. A engenhoca Field cria os descritores title, year, e box_office, que são atributos de classe de Movie. Os dados de cada instância para os campos são armazenados nos atributos de instância _title, _year e _box_office de Movie. Observe a fronteira do pacote checkedlib. O desenvolvedor de Movie não precisa entender todo o maquinário dentro de checkedlib.py.

O Exemplo 21 mostra o código-fonte de Field, com os textos explicativos descrevendo apenas as mudanças nessa versão.

Exemplo 21. metaclass/checkedlib.py: o descritor Field com storage_name e __get__
link:code/24-class-metaprog/checked/metaclass/checkedlib.py[role=include]
  1. Determina storage_name a partir do argumento name.

  2. Se __get__ recebe None como argumento instance, o descritor está sendo lido desde a própria classe gerenciada, não de uma instância gerenciada. Neste caso devolvemos o descritor.

  3. Caso contrário, devolve o valor armazenado no atributo chamado storage_name.

  4. __set__ agora usa setattr para definir ou atualizar o atributo gerenciado.

O Exemplo 22 mostra o código para a metaclasse que controla este exemplo.

Exemplo 22. metaclass/checkedlib.py: tha metaclasse CheckedMeta
link:code/24-class-metaprog/checked/metaclass/checkedlib.py[role=include]
  1. __new__ é o único método implementado em CheckedMeta.

  2. Só melhora a classe se seu cls_dict não incluir __slots__. Se __slots__ já está presente, assume que essa é a classe base Checked e não uma subclasse definida pelo usuário, e cria a classe sem modificações.

  3. Nos exemplos anteriores usamos typing.get_type_hints para obter as dicas de tipo, mas aquilo exige um classe existente como primeiro argumento. Neste ponto, a classe que estamos configurando ainda não existe, então precisamos recuperar __annotations__ diretamente do cls_dict—o espaço de nomes da classe em construção, que o Python passa como último argumento para o __new__ da metaclasse.

  4. Itera sobre type_hints para…​

  5. …​criar um Field para cada atributo anotado…​

  6. …​sobrescreve o item correspondente em cls_dict com a instância de Field…​

  7. …​e acrescenta o storage_name do campo à lista que usaremos para…​

  8. …​preencher o __slots__ no cls_dict—o espaço de nomes da classe em construção.

  9. Por fim, invocamos super().__new__.

A última parte de metaclass/checkedlib.py é a classe base Checked, a partir da qual os usuários dessa biblioteca criarão subclasses para melhorar suas classes, como Movie.

O código desta versão de Checked é o mesmo da Checked em initsub/checkedlib.py (listada no Exemplo 5 e no Exemplo 6), com três modificações:

  1. O acréscimo de um __slots__ vazio, para sinalizar a CheckedMeta.__new__ que esta classe não precisa de processamento especial.

  2. A remoção de __init_subclass__, cujo trabalho agora é feito por CheckedMeta.__new__.

  3. A remoção de __setattr__, que se tornou redundante: o acréscimo de __slots__ à classe definida pelo usuário impede a definição de atributos não declarados.

O Exemplo 23 é a listagem completa da versão final de Checked.

Exemplo 23. metaclass/checkedlib.py: a classe base Checked
link:code/24-class-metaprog/checked/metaclass/checkedlib.py[role=include]

Isso conclui nossa terceira versão de uma fábrica de classes com descritores validados.

A próxima seção trata de algumas questões gerais relacionadas a metaclasses.

Metaclasses no mundo real

Metaclasses são poderosas mas complexas. Antes de se decidir a implementar uma metaclasse, considere os pontos a seguir.

Recursos modernos simplificam ou substituem as metaclasses

Ao longo do tempo, vários casos de uso comum de metaclasses se tornaram redundantes devido a novos recursos da linguagem:

Decoradores de classes

Mais simples de entender que metaclasses, e com menor probabilidade de causar conflitos com classes base e metaclasses.

__set_name__

Elimina a necessidade de uma metaclasse com lógica personalizada para definir automaticamente o nome de um descritor.[16]

__init_subclass__

Fornece uma forma de personalizar a criação de classes que é transparente para o usuário final e ainda mais simples que um decorador—mas pode introduzir conflitos em uma hierarquia de classes complexa.

O dict embutido preservando a ordem de inserção de chaves

Eliminou a principal razão para usar __prepare__: fornecer um OrderedDict para armazenar o espaço de nomes de uma classe em construção. O Python só invoca __prepare__ em metaclasses e então, se fosse necessário processar o espaço de nomes da classe na ordem em que eles aparecem o código-fonte, antes do Python 3.6 era preciso usar uma metaclasse.

Em 2021, todas as versões sob manutenção ativa do CPython suportam todos os recursos listados acima.

Sigo defendendo esses recursos porque vejo muita complexidade desnecessária em nossa profissão, e as metaclasses são uma porta de entrada para a complexidade.

Metaclasses são um recurso estável da linguagem

As metaclasses foram introduzidas no Python em 2002, junto com as assim chamadas "classes com novo estilo", descritores e propriedades. together with so-called "new-style classes," descriptors, and properties.

É impressionante que o exemplo do MetaBunch, postado pela primeira vez por Alex Martelli em julho de 2002, ainda funcione no Python 3.9—a única modificação sendo a forma de especificar a metaclasse a ser usada, algo que no Python 3 é feito com a sintaxe class Bunch(metaclass=MetaBunch):.

Nenhum dos acréscimos que mencionei na Recursos modernos simplificam ou substituem as metaclasses quebrou código existente que usava metaclasses. Mas código legado com metaclasses frequentemente pode ser simplificado através do uso daqueles recursos, especialmente se for possível ignorar versões do Python anteriores à 3.6—versões que não são mais mantidas.

Uma classe só pode ter uma metaclasse

Se sua declaração de classe envolver duas ou mais metaclasses, você verá essa intrigante mensagem de erro:

TypeError: metaclass conflict: the metaclass of a derived class
must be a (non-strict) subclass of the metaclasses of all its bases
(_TypeError: conflito de metaclasses: a metaclasse de uma classe derivada deve ser uma subclasse (não-estrita) das metaclasses de todas as suas bases)

Isso pode acontecer mesmo sem herança múltipla. Por exemplo, a declaração abaixo pode gerar aquele TypeError:

class Record(abc.ABC, metaclass=PersistentMeta):
    pass

Vimos que abc.ABC é uma instância da metaclasse abc.ABCMeta. Se aquela metaclasse Persistent não for uma subclasse de abc.ABCMeta, você tem um conflito de metaclasses.

Há duas maneiras de lidar com esse erro:

  • Encontre outra forma de fazer o que precisa ser feito, evitando o uso de pelo menos uma das metaclasse envolvidas.

  • Escreva a sua própria metaclasse PersistentABCMeta como uma subclasse tanto de abc.ABCMeta quanto de PersistentMeta, usando herança múltipla, e faça dela a única metaclasse de Record.[17]

Tip

Posso aceitar a solução de uma metaclasse com duas metaclasses base, implementada para atender um prazo. Na minha experiência, a programação de metaclasses sempre leva mais tempo que o esperado, tornando essa abordagem arriscada ante um prazo inflexível. Se você fizer isso e cumprir o prazo previsto, seu código pode conter bugs sutis. E mesmo na ausência de bugs conhecidos, essa abordagem deveria ser considerada uma dívida técnica, pelo simples fato de ser difícil de entender e manter.

Metaclasses devem ser detalhes de implementação

Além de type, existem apenas outras seis metaclasses em toda a bilbioteca padrão do Python 3.9. As metaclasses mais conhecidas provavelmnete são abc.ABCMeta, typing.NamedTupleMeta e enum.EnumMeta. Nenhuma delas foi projetada com a intenção de aparecer explicitamente no código do usuário. Podemos considerá-las detalhes de implementação.

Apesar de ser possível fazer metaprogramação bem maluca com metaclasses, é melhor se ater ao princípio do menor espanto, de forma que a maioria dos usuários possa de fato considerar metaclasses detalhes de implementação.[18]

Nos últimos anos, algumas metaclasses na biblioteca padrão do Python foram substituídas por outros mecanismos, sem afetar a API pública de seus pacotes. A forma mais simples de resguardar essas APIs para o futuro é oferecer uma classe regular, da qual usuários podem então criar subclasses para acessar a funcionalidade fornecida pela metaclasse. como fizemos em nossos exemplos.

Para encerrar nossa conversa sobre metaprogramação de classes, vou compartilhar com vocês o pequeno exemplo de metaclasse mais sofisticado que encontrei durante minha pesquisa para esse capítulo.

Um hack de metaclasse com __prepare__

Quando atualizei esse capítulo para a segunda edição, precisava encontrar exemplos simples mas reveladores, para substituir o código de LineItem no exemplo da loja de comida a granel, que não precisava mais de metaclasses desde o Python 3.6.

A ideia de metaclasse mais interessante e mais simples me foi dada por João S. O. Bueno—mais conhecido como JS na comunidade Python brasileira. Uma aplicação de sua ideia é criar uma classe que gera constantes numéricas automaticamente:

link:code/24-class-metaprog/autoconst/autoconst_demo.py[role=include]

Sim, esse código funciona como exibido! Aquilo acima é um doctest em autoconst_demo.py.

Aqui está a classe base fácil de usar AutoConst , e a metaclasse por trás dela, implementadas em autoconst.py:

link:code/24-class-metaprog/autoconst/autoconst.py[role=include]

É só isso.

Claramente, o truque está em WilyDict.

Quando o Python processa o espaço de nomes da classe do usuário e lê banana, ele procura aquele nome no mapeamento fornecido por __prepare__: uma instância de WilyDict. WilyDict implementa __missing__, tratado na [missing_method]. A instância de WilyDict inicialmente não contém uma chave 'banana', então o método __missing__ é acionado. Ele cria um item em tempo real, com a chave 'banana' e o valor 0, e devolve esse valor. O Python se contenta com isso, e daí tenta recuperar 'coconut'. WilyDict imediatamente adiciona aquele item com o valor 1, e o devolve. O mesmo acontece com 'vanilla', que é então mapeado para 2.

Já vimos __prepare__ e __missing__ antes. A verdadeira inovação é a forma como JS as juntou.

Aqui está o código-fonte de WilyDict, também de autoconst.py:

link:code/24-class-metaprog/autoconst/autoconst.py[role=include]

Enquanto experimentava, descobri que o Python procurava __name__ no espaço de nomes da classe em construção, fazendo com que WilyDict acrescentasse um item __name__ e incrementasse __next_value. Eu então inseri uma instrução if em __missing__, para gerar um KeyError para chaves que se parecem com atributos dunder.

O pacote autoconst.py tanto exige quanto ilustra o mecanismo de criação dinâmica de classes do Python.

Me diverti muito adicionando mais funcionalidades a AutoConstMeta e AutoConst, mas em vez de compartilhar meus experimentos, vou deixar vocês se divertirem, brincando com o hack genial de JS.

Aqui estão algumas ideias:

  • Torne possivel obter o nome da constante a partir do valor. Por exemplo, Flavor[2] devolveria 'vanilla'. Você pode fazer isso implementando __getitem__ em AutoConstMeta. Desde o Python 3.9, épossível implementar __class_getitem__ na própria AutoConst.

  • Suporte a iteração sobre a classe, implementando __iter__ na metaclasse. Eu faria __iter__ produzir as constantes na forma de pares (name, value).

  • Implemente uma nova variante de Enum. Isso seria um empreeendimento complexo, pois o pacote enum está cheio de armadilhas, incluindo a metaclasse EnumMeta, com centenas de linhas de código e um método __prepare__ nem um pouco trivial.

Divirta-se!

Note

O método especial __class_getitem__ foi introduzido no Python 3.9 para suportar tipos genéricos, como parte da PEP 585—Type Hinting Generics In Standard Collections (Dicas de Tipos Genéricas em Coleções Padrão) (EN). Graças a __class_getitem__, os desenvolvedores principais do Python não precisaram escrever uma nova metaclasse para que os tipos embutidos implementassem __getitem__, de modo que fosse possível escrever dicas de tipo genéricas, tal como list[int]. Esse é um recurso restrito, mas representativo, de um caso de uso mais amplo para metaclasses: implementar operadores e outros métodos especiais para funcionarem a nível de classes, tal como fazer a própria classe iterável, como as subclasses de Enum.

Para encerrar

Metaclasses, bem como decoradores de classes e __init_subclass__, são úteis para:

  • Registro de subclasses

  • Validação estrutural de subclasses

  • Aplicar decoradores a muitos métodos ao mesmo tempo

  • Serialização de objetos

  • Mapeamento objeto-relacional

  • Persistência baseada em objetos

  • Implementar métodos especiais a nível de classe

  • Implementar recursos de classes encontrados em outras linguagens, tal como traits (traços) (EN) e programação orientada a aspecto

Em alguns casos, a metaprogramação de classes também pode ajudar em questões de desempenho, executando tarefas no momento da importação que de outra forma seriam executadas repetidamente durante a execução.

Para finalizar, vamos nos lembrar do conselho final de Alex Martelli em seu ensaio [waterfowl_essay]:

E não defina ABCs personalizadas (ou metaclasses) em código de produção. Se você sentir uma forte necessidade de fazer isso, aposto que é um caso da síndrome de "todos os problemas se parecem com um prego" em alguém que acabou de ganhar um novo martelo brilhante - você ( e os futuros mantenedores de seu código) serão muito mais felizes se limitando a código simples e direto, e evitando tais profundezas.

Acredito que o conselho de Martelli se aplica não apenas a ABCs e metaclasses, mas também a hierarquias de classe, sobrecarga de operadores, decoradores de funções, descritores, decoradores de classes e fábricas de classes usando __init_subclass__.

Em princípio, essas poderosas ferramentas existem para suportar o desenvolvimento de bibliotecas e frameworks. Naturalmente, as aplicações devem usar tais ferramentas, na forma oferecida pela biblioteca padrão do Python ou por pacotes externos. Mas implementá-las em código de aplicações é frequentemente resultado de uma abstração prematura.

Bons frameworks são extraídos, não inventados.[19]

— David Heinemeier Hansson
criador do Ruby on Rails

Resumo do capítulo

Este capítulo começou com uma revisão dos atributos encontrados em objetos classe, tais como __qualname__ e o método __subclasses__(). A seguir, vimos como a classe embutida type pode ser usada para criar classes durante a execução.

O método especial __init_subclass__ foi introduzido, com a primeira versão de uma classe base Checked, projetada para substituir dicas de tipo de atributos em subclasses definidas pelo usuário por instâncias de Field, que usam construtores para impor o tipo daqueles atributos durante a execução.

A mesma ideia foi implementada com um decorador de classes @checked, que acrescenta recursos a classes definidas pelo usuário, de forma similar ao que pode ser feito com __init_subclass__. Vimos que nem __init_subclass__ nem um decorador de classes podem configurar __slots__ dinamicamente, pois operam apenas após a criação da classe.

Os conceitos de "[momento/tempo de] importação" e "[momento/tempo de] execução" foram esclarecidos com experimentos mostrando a ordem na qual o código Python é executado quando módulos, descritores, decoradores de classe e __init_subclass__ estão envolvidos.

Nossa exploração de metaclasses começou com um explicação geral de type como uma metaclasse, e sobre como metaclasses definidas pelo usuário podem implementar __new__, para personalziar as classes que criam. Vimos então nossa primeira metaclasse personalizada, o clássico exemplo MetaBunch, usando __slots__. A seguir, outro experimento com o tempo de avaliação demonstrou como os métodos __prepare__ e __new__ de uma metaclasse são invocados mais cedo que __init_subclass__ e decoradores de classe, oferecendo oportunidades para uma personalização de classes mais profunda.

A terceira versão de uma fábrica de classes Checked, com descritores Field e uma configuração personalizada de __slots__ foi apresentada, seguida de considerações gerais sobre o uso de metaclasses na prática.

Por fim, vimos o hack AutoConst, inventado por João S. O. Bueno, baseado na brilhante ideia de uma metaclasse com __prepare__ devolvendo um mapeamento que implementa __missing__. Em menos de 20 linhas de código, autoconst.py demonstra o poder da combinação de técnicas de metaprogramação no Python.

Nunca encontrei outra linguagem como o Python, fácil para iniciantes, prática para profissionais e empolgante para hackers. Obrigado, Guido van Rossum e todos que a fazem ser assim.

Leitura complementar

Caleb Hattingh—um dos revisores técnicos desse livro—escreveu o pacote autoslot, fornecendo uma metaclasse para a criação automática do atributo __slots__ em uma classe definida pelo usuário, através da inspeção do bytecode de __init__ e da identificação de todas as atribuições a atributos de self. Além de útil, esse pacote é um excelente exemplo para estudo: são apenas 74 linhas de código em autoslot.py, incluindo 20 linhas de comentários que explicam as partes mais difíceis.

As referências essenciais deste capítulo na documentação do Python são "3.3.3. Personalizando a criação de classe" no capítulo "Modelos de Dados" da Referência da Linguagem Python, que cobre __init_subclass__ e metaclasses. A documentação da classe type na página "Funções Embutidas", e "4.13. Atributos especiais" do capítulo "Tipos embutidos" na Biblioteca Padrão do Python também são leituras fundamentais.

Na Biblioteca Padrão do Python, a documentação do módulo types trata de duas funções introduzidas no Python 3.3, que simplificam a metaprogramação de classes: types.new_class and types.prepare_class.

Decoradores de classes foram formalizados na PEP 3129—Class Decorators (Decoradores de Classes) (EN), escrita por Collin Winter, com a implemetação de referência desenvolvida por Jack Diederich. A palestra "Class Decorators: Radically Simple" (Decoradores de Classes: Radicalmente Simples. Aqui o video (EN)), na PyCon 2009, também de Jack Diederich, é uma rápida introdução a esse recurso. Além de @dataclass, um exemplo interessante—e muito mais simples—de decorador de classes na bilbioteca padrão do Python é functools.total_ordering (EN), que gera métodos especiais para comparação de objetos.

Para metaclasses, a principal referência na documentação do Python é a PEP 3115—Metaclasses in Python 3000 (Metaclasses no Python 3000), onde o método especial __prepare__ foi introduzido.

O Python in a Nutshell, 3ª ed., de Alex Martelli, Anna Ravenscroft, e Steve Holden, é uma referência, mas foi escrito antes da PEP 487—Simpler customization of class creation (PEP 487—Uma personalização mais simples da criação de classes) ser publicada. O principal exemplo de metaclasse no livro—MetaBunch—ainda é válido, pois não pode ser escrito com mecanismos mais simples. O Effective Python, 2ª ed. (Addison-Wesley), de Brett Slatkin, traz vários exemplos atualizados de técnicas de criação de classes, incluindo metaclasses.

Para aprender sobre as origens da metaprogramação de classes no Python, recomento o artigo de Guido van Rossum de 2003, "Unifying types and classes in Python 2.2" (Unificando tipos e classes no Python 2.2) (EN). O texto se aplica também ao Python moderno, pois cobre o quê era então chamado de "novo estilo" de semântica de classes—a semântica default no Python 3—incluindo descritores e metaclasses. Uma das referências citadas por Guido é Putting Metaclasses to Work: a New Dimension in Object-Oriented Programming, de Ira R. Forman e Scott H. Danforth (Addison-Wesley), livro para o qual ele deu cinco estrelas na Amazon.com, acrescentando o seguinte comentário:

Este livro contribuiu para o projeto das metaclasses no Python 2.2

Pena que esteja fora de catálogo; sempre me refiro a ele como o melhor tutorial que conheço para o difícil tópico da herança múltipla cooperativa, suportada pelo Python através da função super().[20]

Se você gosta de metaprogramação, talvez gostaria que o Python suportasse o recurso definitivo de metaprogramação: macros sintáticas, como as oferecidas pela família de linguagens Lisp e—mais recentemente—pelo Elixir e pelo Rust. Macros sintáticas são mais poderosas e menos sujeitas a erros que as macros primitivas de substituição de código da linguagem C. Elas são funções especiais que reescrevem código-fonte para código padronizado, usando uma sintaxe personalizada, antes da etapa de compilação, permitindo a desenvolvedores introduzir novas estruturas na linguagem sem modificar o compilador. Como a sobrecarga de operadores, macros sintáticas podem ser mal usadas. Mas, desde que a comunidade entenda e gerencie as desvantagens, elas suportam abstrações poderosas e amigáveis, como as DSLs (Domain-Specific Languages—Linguagens de Domínio Específico). Em setembro de 2020, Marc Shannon, um dos desenvolvedores principais do Python, publicou a PEP 638—Syntactic Macros (Macros Sintáticas) (EN), defendendo exatamente isso. Um ano após sua publicação inicial (quando escrevo essas linhas), a PEP 638 ainda era um rascunho e não havia discussões contínuas sobre ela. Claramente não é uma prioridade muito alta entre os desenvolvedores principais do Python. Eu gostaria de ver a PEP 638 sendo melhor discutida e, por fim, aprovada. Macros sintáticas permitiriam à comunidade Python experimentar com novos recursos controversos, tal como o "operador morsa" (operador walrus) (PEP 572 (EN)), correspondência/casamento de padrões (PEP 634 (EN)) e regras alternativas para avaliação de dicas de tipo (PEPs 563 (EN) e 649 (EN)), antes que se fizessem modificações permanentes no núcleo da linguagem. Nesse meio tempo, podemos sentir o gosto das macros sintáticas com o pacote MacroPy.

Ponto de vista

Vou iniciar o último ponto de vista no livro com uma longa citação de Brian Harvey e Matthew Wright, dois professores de ciência da computação da Universidade da California (Berkeley e Santa Barbara). Em seu livro, Simply Scheme: Introducing Computer Science ("Simplesmente Scheme: Introduzindo a Ciência da Computação") (MIT Press), Harvey e Wright escreveram:

Há duas escolas de pensamento sobre o ensino de ciência da computação. Podemos representar as duas visões de uma forma caricatual, assim:

  1. A visão conservadora: Programas de computador se tornaram muito grandes e complexos para serem apreendidos pela mente humana. Portanto, a tarefa da educação na ciência da computação é ensinar os estudantes como se disciplinarem, de tal forma que 500 programadores medíocres possam se juntar e produzir um programa que atende suas especificações.

  2. A visão radical: Programas de computador se tornaram muito grandes e complexos para serem apreendidos pela mente humana. Portanto, a tarefa da educação na ciência da computação é ensinar os estudantes como expandir suas mentes até que os programas caibam ali, aprendendo a pensar com um vocabulário de ideias maiores, mais poderosas e mais flexíveis que aquelas óbvias. Cada unidade de pensamento programático deve gerar uma grande recompensa para as capacidades do programa.[21]

— Brian Harvey and Matthew Wright
no prefácio de Simply Scheme

As descrições exageradas de Harvey e Wright versam sobre o ensino de ciência da computação, mas também se aplicam ao projeto de linguagens de programação. Nesse ponto você já deve ter adivinhado que eu concordo com a visão "radical", e acredito que o Python foi projetado nesse espírito.

A ideia de propriedade é um grande passo adiante, comparado com a abordagem "métodos de acesso desde o início", praticamente exigida em Java e suportada pela geração de getters/setters através de atalhos do teclado por IDEs Java. A principal vantagem das propriedades é nos permitir começar a criar nossos programas simplesmente expondo atributos publicamente—no espírito do KISS—sabendo que um atributo público pode se tornar uma propriedade a qualquer momento sem quebrar código existente. Mas a ideia de descritor vai muito além disso, fornecendo um framework para abstrair lógica repetitiva para acessar atributos. Esse framework é tão eficiente que mecanismos essenciais do Python o utilizam por baixo dos panos.

Outra ideia poderosa são as funções como objetos de primeira classe, pavimentando o caminho para funções de ordem superior. E acontece que a combinação de descritores e funções de ordem superior permite a unificação de funções e métodos. O __get__ de uma função produz um objeto método em tempo real, vinculando a instância ao argumento self. Isso é elegante.[22]

Por fim, temos a ideia de classes como objetos de primeira classe. É uma façanha marcante do projeto, que uma linguagem acessível para um iniciante forneça abstrações poderosas, tais como fábricas de classe, decoradores de classe, e metaclasses completas e definidas pelo usuário. Melhor ainda, os recursos avançados estão integrados de forma a não afetar a adequação do Python para programação casual (eles na verdade ajudam nisso, por trás da cortina). A conveniência e o sucesso de frameworks como o Django e o SQLAlchemy devem muito às metaclasses. Ao longo dos anos, a metaprogramação de classes em Python está se tornando cada vez mais simples, pelo menos para os casos de uso comuns. Os melhores recursos da linguagem são aqueles que beneficiam a todos, mesmo que alguns usuários do Python não os conheçam. Mas esses usuários sempre podem aprender, e criar a próxima grande biblioteca.

Espero notícias sobre suas contribuições ao ecossistema e à comunidade do Python!


1. Citação extraída do capítulo 2, Expression ("Expressão"), página 10, de _The Elements of Programming Style, Second Edition (NT: "Elementos de Estilo de Programação"; não encontramos edição traduzida deste livro.)
2. Isso não quer dizer que a PEP 487 quebrou código que usava aqueles recursos, mas apenas que parte do código que utilizava decoradores de classe ou metaclasses antes do Python 3.6 pode agora ser refatorado para usar classes comuns, resultando em um código mais simples e possivelmente mais eficiente.
3. Agradeço a meu amigo J. S. O. Bueno por ter contribuído com esse exemplo.
4. Não acrescentei dicas de tipo aos argumentos porque os tipos reais são Any. Escrevi a dica to tipo devolvido porque em caso contrário, o Mypy não verificaria o código dentro do método.
5. Isso é verdade para qualquer objeto, exceto quando sua classe sobrepõe os métodos __str__ ou __repr__, herdados de object, por uma implementação que não funcione.
6. Essa solução evita usar None como default. Evitar valores nulos é uma boa ideia. Em geral, eles são difíceis de evitar, mas em alguns casos isso é fácil. Tanto no Python quanto no SQL, prefiro representar dados ausentes em um campo de texto como um string vazia em vez de None ou NULL. Aprender Go reforçou essa ideia: em Go, variáveis e campos struct de tipos primitivos são inicializados por default com um "valor zero" (zero value). Se você estiver curiosa, veja a página "Zero values" ("Valores zero") (EN) no Tour of Go ("Tour do Go") online
7. Na minha opinião, callable deveria se tornar adequado para dicas de tipo. Em 6 de maio de 2021, quando essa nota foi escrita, essa ainda era uma questão aberta (EN).
8. Como mencionado em [good_poison_pill_tip], o objeto Ellipsis é um valor sentinela conveniente e seguro. Ele existe no Python há muito tempo, mas recentemente mais usos tem sido encontrados para ele, como vemos nas dicas de tipo e no NumPy.
9. NT: Em 17 de setembro de 2023, a primeira frase está traduzida de forma confusa na documentação em português. Optamos por traduzir aqui diretamente da documentação em inglês.
10. O sutil conceito de descritor dominante foi explicado na [overriding_descriptor_sec].
11. Essa justificativa aparece no resumo da PEP 557–Data Classes (Classes de Dados) (EN), para explicar porque ela foi implementada como um decorador de classes.
12. Compare com a instrução import em Java, que é apenas uma declaração para informar o compilador que determinados pacotes são necessários.
13. Não estou dizendo que é uma boa ideia abrir uma conexão com um banco de dados só porque o módulo foi importado, apenas apontando que isso pode ser feito.
14. Mensagem a comp.lang.python, assunto: "Acrimony in c.l.p." (animosidade no c.l.p.). Essa é outra parte da mesma mensagem de 23 de dezembro de 2002, citada na [preface_sec]. O TimBot estava inspirado naquele dia.
15. Os autores gentilmente me deram permissão para usar seu exemplo. MetaBunch apareceu pela primeira vez em uma mensagem enviada por Martelli para o grupo comp.lang.python, em 7 de julho de 2002, com o assunto "a nice metaclass example (was Re: structs in python)" (um belo exmeplo de metaclasse (era Re: structs no python)), na sequência de uma discussão sobre estruturas de dados similares a registros no Python. O código original de Martelli, para Python 2.2, ainda roda após uma única modificação: para usar uma metaclasse no Python 3, é necessário usar o argumento nomeado metaclass na declaração da classe (por exemplo, Bunch(metaclass=MetaBunch)), em vez da convenção antiga, que era adicionar um atributo __metaclass__ no corpo da classe.
16. Na primeira edição de Python Fluente, as versões mais avançadas da classe LineItem usavam uma metaclasse apenas para definir o nome do armazenamento dos atributos. Veja o código nas metaclasses do exemplo da comida a granel, no repositório de código da primeira edição.
17. Se você sentiu vertigem ao ponderar sobre as implicações de herança múltipla com metaclasses, bom para você. Eu também passaria longe dessa solução.
18. Eu ganhei a vida por alguns anos escrevendo código para Django, antes de resolver estudar como os campos dos modelos Django eram implementados. Só então aprendi sobre descritores e metaclasses.
19. Essa frase é muito citada. Encontrei uma citação direta antiga em um post de 2005 no blog de DHH.
20. Comprei um exemplar usado, e achei uma leitura muito desafiadora.
21. Brian Harvey e Matthew Wright, Simply Scheme (MIT Press, 1999), p. xvii. O texto completo está disponível em Berkeley.edu (EN).
22. Machine Beauty: Elegance and the Heart of Technology ("Beleza de Máquina: A Elegância e o Coração da Tecnologia"), de David Gelernter (Basic Books), começa com uma discussão intrigante sobre elegância e estética em obras de engenharia, de pontes a software. Os capítulos posteriores não são tão bons, mas o início vale o preço.