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]
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. |
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.
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 chamadaMeta. O__qualname__deMetaéOx.Meta, mas seu__name__é apenasMeta. 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 |
Agora, se classe é um objeto, o que é a classe de uma classe?
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 * 2Usando 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çãoclass. 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 |
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.
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 = ownerChato… 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.
record_factory, uma fábrica de classes simpleslink:code/24-class-metaprog/factories.py[role=include]-
A fábrica pode ser chamada como
namedtuple: nome da classe, seguido dos nomes dos atributos separados por espaços, em um única string. -
Um
repragradável. -
Instâncias são iteráveis, então elas podem ser convenientemente desempacotadas em uma atribuição…
-
…ou quando são passadas para funções como
format. -
Uma instância do registro é mutável.
-
A classe recém-criada herda de
object—não tem qualquer relação com nossa fábrica.
link:code/24-class-metaprog/factories.py[role=include]-
O usuário pode fornecer os nomes dos campos como uma string única ou como um iterável de strings.
-
Aceita argumentos como os dois primeiros de
collections.namedtuple; devolvetype—isto é, uma classe que se comporta como umatuple. -
Cria uma tupla de nomes de atributos; esse será o atributo
__slots__da nova classe. -
Essa função se tornará o método
__init__na nova classe. Ela aceita argumentos posicionais e/ou nomeados.[4] -
Produz os valores dos campos na ordem dada por
__slots__. -
Produz um
repragradável, iterando sobre__slots__eself. -
Monta um dicionário de atributos de classe.
-
Cria e devolve a nova classe, invocando o construtor de
type. -
Converte
namesseparados por espaços ou vírgulas em uma lista destr.
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 |
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.
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 |
O Exemplo 3 mostra como usar Checked para criar uma classe Movie.
Movie de Checkedlink:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]-
Movieherda deChecked—que definiremos mais tarde, no Exemplo 5. -
Cada atributo é anotado com um construtor. Aqui usei tipos embutidos.
-
Instâncias de
Moviedevem ser criadas usando argumentos nomeados. -
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 tiposEm um arquivo de código fonte .py contendo uma instância 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 Por outro lado, se você declarar um campo de uma subclasse de |
Vamos ver agora a implementação de checkedlib.py.
A primeira classe é o descritor Field, como mostra o Exemplo 4.
Fieldlink:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]-
Lembre-se, desde o Python 3.9, o tipo
Callablepara anotações é a ABC emcollections.abc, e não o descontinuadotyping.Callable. -
Essa é a dica de tipo
Callablemínima; o parâmetro de tipo e o tipo devolvido paraconstructorsão ambos implicitamenteAny. -
Para verificação durante a execução, usamos o embutido
callable.[7] O teste contratype(None)é necessário porque o Python entendeNoneem um tipo comoNoneType, a classe deNone(e portanto invocável), mas esse é um construtor inútil, que apenas devolveNone. -
Se
Checked.__init__definirvaluecomo…(o objeto embutidoEllipsis), invocamos o construtor sem argumentos. -
Caso contrário, invocamos o
constructorcom ovaluedado. -
Se
constructorgerar qualquer dessas exceções, geramos umTypeErrorcom uma mensagem útil, incluindo os nomes do campo e do construtor; por exemplo,'MMIX' não é compatível com year:int. -
Se nenhuma exceção for gerada, o
valueé armazenado noinstance.__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 |
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.
Checkedlink:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]-
Escrevi este método de classe para ocultar a chamada a
typing.get_type_hintsdo resto da classe. Se precisasse suportar apenas versões do Python ≥ 3.10, invocariainspect.get_annotationsem vez disso. Reveja a [problems_annot_runtime_sec] para uma discussão dos problemas com essas funções. -
__init_subclass__é chamado quando uma subclasse da classe atual é definida. Ele recebe aquela nova subclasse como seu primeiro argumento—e por isso nomeei o argumentosubclassem vez do habitualcls. Para mais informações sobre isso, veja __init_subclass__ não é um método de classe típico. -
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]. -
Itera sobre
nameeconstructorem cada campo… -
…criando um atributo em
subclasscom aquelenamevinculado a um descritorField, parametrizado comnameeconstructor. -
Para cada
namenos campos da classe… -
…obtém o
valuecorrespondente dekwargse o remove dekwargs. Usar…(o objetoEllipsis) como default nos permite distinguir entre argumentos com valorNonede argumentos ausentes.[8] -
Essa chamada a
setattracionaChecked.__setattr__, apresentado no Exemplo 6. -
Se houver itens remanescentes em
kwargs, seus nomes não correspondem a qualquer dos campos declarados, e__init__vai falhar. -
Esse erro é informado por
__flag_unknown_attrs, listado no Exemplo 6. Ele recebe um argumento*namescom os nomes de atributos desconhecidos. Usei um único asterisco em*kwargs, para passar suas chaves como uma sequência de argumentos.
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.
Checkedlink:code/24-class-metaprog/checked/initsub/checkedlib.py[role=include]-
Intercepta qualquer tentativa de definir um atributo de instância. Isso é necessário para evitar a definição de um atributo desconhecido.
-
Se o
namedo atributo é conhecido, busca odescriptorcorrespondente. -
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 comoField.[10] -
Caso contrário, o atributo
nameé desconhecido, e uma exceção será gerada por__flag_unknown_attrs. -
Cria uma mensagem de erro útil, listando todos os argumentos inesperados, e gera um
AttributeError. Este é um raro exemplo do tipo especialNoReturn, tratado na [noreturn_sec]. -
Cria um
dicta partir dos atributos de um objetoMovie. Eu chamaria este método de_as_dict, mas segui a convenção iniciada com o método_asdictemcollections.namedtuple. -
Implementar um
__repr__agradável é a principal razão para ter_asdictneste 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.
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.
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).
Movie decorada com @checkedlink: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.
link:code/24-class-metaprog/checked/decorator/checkeddeco.py[role=include]-
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. -
_fieldsagora é uma função de alto nível definida mais tarde no módulo (no Exemplo 9). -
Substituir cada atributo devolvido por
_fieldspor uma instância do descritorFieldé o que__init_subclass__fazia no Exemplo 5. Aqui há mais trabalho a ser feito… -
Cria um método de classe a partir de
_fields, e o adiciona à classe decorada. O comentáriotype: ignoreé necessário, porque o Mypy reclama quetypenão tem um atributo_fields. -
Funções ao nível do módulo, que se tornarão métodos de instância da classe decorada.
-
Adiciona cada um dos
instance_methodsacls. -
Devolve a
clsdecorada, 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.
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.
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:
-
Analisa o código-fonte de módulo .py em uma passagem, de cima até embaixo. É aqui que um
SyntaxErrorpode ocorrer. -
Compila o bytecode a ser executado.
-
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.
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 |
Vamos começar examinando builderlib.py, dividido em duas partes: o Exemplo 10 e o Exemplo 11.
link:code/24-class-metaprog/evaltime/builderlib.py[role=include]-
Essa é uma fábrica de classes para implementar…
-
…um método
__init_subclass__. -
Define uma função para ser adicionada à subclasse na atribuição abaixo.
-
Um decorador de classes.
-
Função a ser adicionada à classe decorada.
-
Devolve a classe recebida como argumento.
Continuando builderlib.py no Exemplo 11…
link:code/24-class-metaprog/evaltime/builderlib.py[role=include]-
Uma classe descritora para demonstrar quando…
-
…uma instância do descritor é criada, e quando…
-
…
__set_name__será invocado durante a criação da classeowner. -
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 endObserve 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).
link:code/24-class-metaprog/evaltime/evaldemo.py[role=include]-
Aplica um decorador.
-
Cria uma subclasse de
Builderpara acionar seu__init_subclass__. -
Instancia o descritor.
-
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.
>>> 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-
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. -
Isso sinaliza que o Python começou a ler o corpo de
Klass. Neste momento o objeto classe ainda não existe. -
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__. -
Neste ponto, a função embutida do Python
type.__new__já criou o objetoKlasse invoca__set_name__em cada instância das classes do descritor que oferecem aquele método, passandoKlasscomo argumentoowner. -
type.__new__então chama__init_subclass__na superclasse deKlass, passandoKlasscomo único argumento. -
Quando
type.__new__devolve o objeto classe, o Python aplica o decorador. Neste exemplo, a classe devolvida pordecoestá vinculada aKlassno 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).
$ ./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-
As 10 primeiras linhas—incluindo essa—são as mesma que aparecem no Exemplo 13.
-
Acionado por
super().__init__()emKlass.__init__. -
Acionado por
obj.method_a()emmain; omethod_afoi injetado porSuperA.__init_subclass__. -
Acionado por
obj.method_b()emmain;method_bfoi injetado pordeco. -
Acionado por
obj.attr = 999emmain.
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.
[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]
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.
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.
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 |
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.
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.
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 = yPara 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.
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.
MetaBunch e a classe Bunchlink:code/24-class-metaprog/metabunch/from3.6/bunch.py[role=include]-
Para criar uma nova metaclasse, herdamos de
type. -
__new__funciona como um método de classe, mas a classe é uma metaclasse, então gosto de nomear o primeiro argumentometa_cls(mcsé uma alternativa comum). Os três argumentos restantes são os mesmos da assinatura de três argumentos detype(), quando chamada diretamente para criar uma classe. -
defaultsvai manter um mapeamento de nomes de atributos e seus valores default. -
Isso irá ser injetado na nova classe.
-
Lê
defaultse define o atributo de instância correspondente, com o valor extraído dekwargs, ou um valor default. -
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 dekwargse tentar defini-lo na instância, gerando propositalmente umAttributeError. -
__repr__devolve uma string que se parece com uma chamada ao construtor—por exemplo,Point(x=3), omitindo os argumentos nomeados com valores default. -
Inicializa o espaço de nomes para a nova classe.
-
Itera sobre o espaço de nomes da classe do usuário.
-
Se um
namedunder (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__. -
Se
namenão for um dunder, acrescentanamea__slots__e armazena seuvalueemdefaults. -
Cria e devolve a nova classe.
-
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.
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.
link:code/24-class-metaprog/evaltime/evaldemo_meta.py[role=include]-
Importa
MetaKlassde metalib.py, que veremos no Exemplo 18. -
Declara
Klasscomo uma subclasse deBuildere uma instância deMetaKlass. -
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 |
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.
NosyDictlink: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:
|
MetaKlasslink:code/24-class-metaprog/evaltime/metalib.py[role=include]-
__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__. -
O Python invoca
__prepare__em uma metaclasse para obter um mapeamento, onde vai manter o espaço de nomes da classe em construção. -
Devolve uma instância de
NosyDictpara ser usado como o espaço de nomes. -
cls_dicté uma instância deNosyDictdevolvida por__prepare__. -
type.__new__exige umdictreal como último argumento, então passamos a ele o atributodatadeNosyDict, herdado deUserDict. -
Injeta um método na classe recém-criada.
-
Como sempre,
__new__precisa devolver o objeto que acaba de ser criado—neste caso, a nova classe. -
Definir
__repr__em uma metaclasse permite personalizar orepr()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 endMuitas coisas acontecem quando importamos evaldemo_meta.py, como visto no Exemplo 19.
>>> 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-
As linhas antes disso são resultado da importação de builderlib.py e metalib.py.
-
O Python invoca
__prepare__para iniciar o processamento de uma instruçãoclass. -
Antes de analisar o corpo da classe, o Python acrescenta
__module__e__qualname__ao espaço de nomes de uma classe em construção. -
A instância do descritor é criada…
-
…e vinculada a
attrno espaço de nomes da classe. -
Os métodos
__init__e__repr__são definidos e adicionados ao espaço de nomes. -
Após terminar o processamento do corpo da classe, o Python chama
MetaKlass.__new__. -
__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).
$ ./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-
As primeiras 21 linhas—incluindo esta—são as mesmas que aparecem no Exemplo 19.
-
Acionado por
obj.method_c()emmain;method_cfoi injetado porMetaKlass.__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.
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.
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
Checkednã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 deField.__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__.
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.
Field com storage_name e __get__link:code/24-class-metaprog/checked/metaclass/checkedlib.py[role=include]-
Determina
storage_namea partir do argumentoname. -
Se
__get__recebeNonecomo argumentoinstance, o descritor está sendo lido desde a própria classe gerenciada, não de uma instância gerenciada. Neste caso devolvemos o descritor. -
Caso contrário, devolve o valor armazenado no atributo chamado
storage_name. -
__set__agora usasetattrpara definir ou atualizar o atributo gerenciado.
O Exemplo 22 mostra o código para a metaclasse que controla este exemplo.
CheckedMetalink:code/24-class-metaprog/checked/metaclass/checkedlib.py[role=include]-
__new__é o único método implementado emCheckedMeta. -
Só melhora a classe se seu
cls_dictnão incluir__slots__. Se__slots__já está presente, assume que essa é a classe baseCheckede não uma subclasse definida pelo usuário, e cria a classe sem modificações. -
Nos exemplos anteriores usamos
typing.get_type_hintspara 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 docls_dict—o espaço de nomes da classe em construção, que o Python passa como último argumento para o__new__da metaclasse. -
Itera sobre
type_hintspara… -
…criar um
Fieldpara cada atributo anotado… -
…sobrescreve o item correspondente em
cls_dictcom a instância deField… -
…e acrescenta o
storage_namedo campo à lista que usaremos para… -
…preencher o
__slots__nocls_dict—o espaço de nomes da classe em construção. -
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:
-
O acréscimo de um
__slots__vazio, para sinalizar aCheckedMeta.__new__que esta classe não precisa de processamento especial. -
A remoção de
__init_subclass__, cujo trabalho agora é feito porCheckedMeta.__new__. -
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.
Checkedlink: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 são poderosas mas complexas. Antes de se decidir a implementar uma metaclasse, considere os pontos a seguir.
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
dictembutido preservando a ordem de inserção de chaves -
Eliminou a principal razão para usar
__prepare__: fornecer umOrderedDictpara 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.
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.
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):
passVimos 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
PersistentABCMetacomo uma subclasse tanto deabc.ABCMetaquanto dePersistentMeta, usando herança múltipla, e faça dela a única metaclasse deRecord.[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. |
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.
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__emAutoConstMeta. Desde o Python 3.9, épossível implementar__class_getitem__na própriaAutoConst. -
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 pacoteenumestá cheio de armadilhas, incluindo a metaclasseEnumMeta, com centenas de linhas de código e um método__prepare__nem um pouco trivial.
Divirta-se!
|
Note
|
O método especial |
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]
criador do Ruby on Rails
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.
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.
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:
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.
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]
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!
Any. Escrevi a dica to tipo devolvido porque em caso contrário, o Mypy não verificaria o código dentro do método.
__str__ ou __repr__, herdados de object, por uma implementação que não funcione.
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
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).
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.
import em Java, que é apenas uma declaração para informar o compilador que determinados pacotes são necessários.
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.
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.



