Aprender sobre descritores não apenas dá acesso a um conjunto maior de ferramentas, também cria uma compreensão melhor sobre o funcionamento do Python e uma apreciação pela elegância de seu design.[1]
guru e mantenedor do Python
Descritores são uma forma de reutilizar a mesma lógica de acesso em múltiplos atributos. Por exemplo, são descritores os campos de registros em um ORM (Object Relational Mapping, Mapeamento Objeto-Relacional) como o SQLAlchemy ou ORM do Django. Nestes sistemas, os descritores controlam a conversão e validação de dados dos atributos de objetos Python para campos nas tabelas do banco de dados.
Um descritor é uma classe que implementa um protocolo dinâmico, composto pelos métodos __get__, __set__, e __delete__. A classe property implementa o protocolo de descritor completo. Como habitual em protocolos dinâmicos, implementações parciais são aceitáveis. E, na verdade, a maioria dos descritores que vemos em código real implementa apenas
__get__ e __set__, e muitos têm só um destes métodos.
Descritores são um recurso característico de Python, presentes não apenas no nível das aplicações, mas também na infraestrutura da linguagem. Funções definidas pelo usuário são descritores. Veremos como o protocolo do descritor permite que métodos operem como métodos vinculados ou funções, dependendo de como são acessados.
Entender os descritores é crucial para dominar Python. Este capítulo é sobre isso.
Nas próximas páginas vamos refatorar o exemplo da loja de alimentos orgânicos a granel, visto na [prop_validation_sec], substituindo propriedades por descritores. Isto tornará mais fácil reutilizar a lógica de validação de atributos em diferentes classes.
Vamos estudar os conceitos de descritores dominantes e não dominantes, e entender que as funções de Python são descritores. Para finalizar, veremos algumas dicas para a implementação de descritores.
O exemplo do descritor
Quantity, na LineItem versão #4: Nomeando atributos de armazenamento automaticamente, ficou muito mais simples graças ao
método especial __set_name__, adicionado ao protocolo de descritor no Python
3.6. Naquela mesma seção, removi o exemplo da fábrica de propriedades, pois ele se
tornou irrelevante: o ponto ali era mostrar uma solução alternativa para o
problema de Quantity, mas com __set_name__ o exemplo ficou redundante.
Removi a classe AutoStorage, que aparecia na LineItem versão #5: um novo tipo descritor pelo mesmo motivo.
Como vimos na [coding_prop_factory_sec], uma fábrica de propriedades é uma forma de evitar código repetitivo de getters e setters, aplicando padrões de programação funcional.
Uma fábrica de propriedades é uma função de ordem superior que cria um conjunto de funções de acesso parametrizadas e constrói uma instância de propriedade customizada, com clausuras para preservar configurações como storage_name.
A forma orientada a objetos de resolver o mesmo problema é uma classe descritora.
Vamos seguir com a série de exemplos LineItem de onde paramos, na [coding_prop_factory_sec], refatorando a fábrica de propriedades quantity em uma classe descritora Quantity.
Isso vai torná-la mais fácil de usar.
Como dito na introdução, uma classe que implemente um método __get__, um __set__, ou um
__delete__ é um descritor. Para usar um descritor, declaramos instâncias dele como atributos em outra classe.
Vamos criar um descritor Quantity, e a classe LineItem vai usar duas instâncias de Quantity: uma para gerenciar o atributo weight, a outra para price. Um diagrama ajuda: dê uma olhada na Diagrama de classe UML para LineItem usando uma classe descritora chamada Quantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias de Quantity vinculadas à classe LineItem (são atributos da classe), mas cada instância de LineItem também tem atributos weight e price, onde estes valores são armazenados na própria instância..
LineItem usando uma classe descritora chamada Quantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias de Quantity vinculadas à classe LineItem (são atributos da classe), mas cada instância de LineItem também tem atributos weight e price, onde estes valores são armazenados na própria instância.Note que a palavra weight aparece duas vezes na Diagrama de classe UML para LineItem usando uma classe descritora chamada Quantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias de Quantity vinculadas à classe LineItem (são atributos da classe), mas cada instância de LineItem também tem atributos weight e price, onde estes valores são armazenados na própria instância., pois na verdade há dois atributos diferentes chamados weight: um é um atributo de classe de LineItem, o outro é um atributo de instância que existirá em cada objeto LineItem. O mesmo se aplica a price.
Implementar e usar descritores envolve vários componentes, então é útil ser preciso ao nomeá-los. Vou utilizar termos e definições abaixo nas descrições dos exemplos desse capítulo. Será mais fácil entendê-los após ver o código, mas quis colocar todas as definições no início, para você poder voltar a elas quando necessário.
- Classe descritora (descriptor class)
-
Uma classe que implementa o protocolo de descritor. Por exemplo,
Quantityna Diagrama de classe UML paraLineItemusando uma classe descritora chamadaQuantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias deQuantityvinculadas à classeLineItem(são atributos da classe), mas cada instância deLineItemtambém tem atributosweighteprice, onde estes valores são armazenados na própria instância.. - Classe gerenciada (managed class)
-
A classe onde as instâncias do descritor são declaradas, como atributos de classe. Na Diagrama de classe UML para
LineItemusando uma classe descritora chamadaQuantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias deQuantityvinculadas à classeLineItem(são atributos da classe), mas cada instância deLineItemtambém tem atributosweighteprice, onde estes valores são armazenados na própria instância.,LineItemé a classe gerenciada. - Instância do descritor (descriptor instance)
-
Cada instância de uma classe descritora declarada como um atributo de classe na classe gerenciada. Na Diagrama de classe UML para
LineItemusando uma classe descritora chamadaQuantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias deQuantityvinculadas à classeLineItem(são atributos da classe), mas cada instância deLineItemtambém tem atributosweighteprice, onde estes valores são armazenados na própria instância., cada instância do descritor está representada pela seta de composição com um nome sublinhado (na UML, o sublinhado indica um atributo de classe). Os diamantes pretos tocam a classeLineItem, que contém as instâncias do descritor. - Instância gerenciada (managed instance)
-
Uma instância da classe gerenciada. Neste exemplo, instâncias de
LineItemsão as instâncias gerenciadas (elas não aparecem no diagrama de classe). - Atributo de armazenamento (storage attribute)
-
Um atributo da instância gerenciada que guarda o valor de um atributo gerenciado para aquela instância específica. Na Diagrama de classe UML para
LineItemusando uma classe descritora chamadaQuantity. Atributos sublinhados no UML são atributos de classe. Observe que weight e price são instâncias deQuantityvinculadas à classeLineItem(são atributos da classe), mas cada instância deLineItemtambém tem atributosweighteprice, onde estes valores são armazenados na própria instância., os atributos de instânciaweightepricedeLineItemsão atributos de armazenamento. Eles são diferentes das instâncias do descritor, que são sempre atributos de classe. - Atributo gerenciado (managed attribute)
-
Um atributo público na classe gerenciada que é controlado por uma instância de um descritor, com os valores preservados em atributos de armazenamento. Uma instância do descritor e um atributo de armazenamento fornecem a infraestrutura para um atributo gerenciado.
É importante entender que instâncias de Quantity são atributos de classe de LineItem. Este ponto fundamental é realçado pelas engenhocas (mills) e bugigangas (gizmos) na Diagrama de classe UML anotado com MGN (Mills & Gizmos Notation—Notação de Engenhocas e Bugigangas): classes são engenhocas que produzem bugigangas—as instâncias. A engenhoca Quantity produz duas bugigangas de cabeça redonda, que são anexadas à engenhoca LineItem: weight e price. A engenhoca LineItem produz bugigangas retangulares que têm seus próprios atributos weight e price, onde aqueles valores são armazenados..
Quantity produz duas bugigangas de cabeça redonda, que são anexadas à engenhoca LineItem: weight e price. A engenhoca LineItem produz bugigangas retangulares que têm seus próprios atributos weight e price, onde aqueles valores são armazenados.Após explicar descritores várias vezes, percebi que a UML não é muito boa para mostrar as relações entre classes e instâncias, tal como a relação entre uma classe gerenciada e as instâncias do descritor.[2] Daí inventei minha própria "linguagem", a Notação Engenhocas e Bugigangas (MGN), que uso para anotar diagramas UML.
Desenhei a MGN para tornar mais evidente a diferença entre classes e instâncias. Veja a Esboço MGN mostrando a classe LineItem produzindo três instâncias, e Quantity produzindo duas. Uma instância de Quantity está recuperando um valor armazenado em uma instância de LineItem.. Na MGN, uma classe aparece como uma "engenhoca", (mill) uma máquina complicada que produz bugigangas (gizmos). Classes/engenhocas são máquinas com alavancas e mostradores. As bugigangas são as instâncias, e elas têm uma aparência mais simples. Quando este livro é produzido em cores, as bugigangas têm a mesma cor da engenhoca que as produziu.
LineItem produzindo três instâncias, e Quantity produzindo duas. Uma instância de Quantity está recuperando um valor armazenado em uma instância de LineItem.Para este exemplo, desenhei instâncias de LineItem como linhas em uma fatura tabular, com três células representando os três atributos (description, weight e price). Como as instâncias de Quantity são descritores, elas têm uma lente de aumento para ler (get) os valores, e uma garra para escrever (set) os valores. Quando chegarmos às metaclasses, você me agradecerá por esses desenhos.
Chega de rabiscos por enquanto. Aqui está o código: o bulkfood_v3.py: o descritor Quantity não aceita valores negativos mostra a classe descritora Quantity, e o bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem lista a nova classe LineItem usando duas instâncias de Quantity.
Quantity não aceita valores negativoslink:../code/23-descriptor/bulkfood/bulkfood_v3.py[role=include]-
O descritor é um recurso baseado em protocolo: não é necessário herdar de uma classe específica, basta implementar
__get__ou__set__. -
Cada instância de
Quantityterá um atributostorage_name: é o nome do atributo de armazenamento que vai preservar o valor nas instâncias gerenciadas. -
O
__set__é invocado quando ocorre uma tentativa de atribuir um valor a um atributo gerenciado. Aqui,selfé a instância do descritorQuantity, (isto é,LineItem.weightouLineItem.price),instanceé a instância gerenciada (uma instância deLineItem) evalueé o valor que está sendo atribuído. -
Precisamos armazenar o valor do atributo diretamente no
__dict__; invocarsetattr(instance, self.storage_name, value)dispararia novamente o método__set__, levando a uma recursão infinita. -
Precisamos implementar
__get__, pois o nome do atributo gerenciado pode não ser igual aostorage_name. O argumentoownerserá explicado a seguir.
Implementar __get__ é necessário porque um usuário poderia escrever algo assim:
class Casa:
quartos = Quantity('cômodos')Na classe Casa, o atributo gerenciado é quartos, mas o atributo de armazenamento é cômodos.
Dada uma instância de Casa chamada meu_lar, acessar e modificar meu_lar.quartos passa pela instância do descritor Quantity vinculado a quartos, mas acessar e modificar meu_lar.cômodos não passa pelo descritor.
Observe que __get__ recebe três argumentos: self, instance e owner. O argumento owner é uma referência à classe gerenciada (por exemplo, LineItem), e é útil se você quiser que o descritor suporte o acesso a um atributo de classe—talvez para emular o comportamento default de Python, de procurar um atributo de classe quando o nome não é encontrado na instância.
Se um atributo gerenciado como weight, é acessado através da classe como
LineItem.weight, o método __get__ do descritor recebe None no argumento instance.
Para suportar introspecção e outras técnicas de metaprogramação pelo usuário, é uma boa prática fazer __get__ devolver a instância do descritor quando o atributo gerenciado é acessado através da classe. Para fazer isso, escreveríamos __get__ assim:
def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.storage_name]O bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem demonstra o uso de Quantity em LineItem.
Quantity gerenciam atributos em LineItemlink:../code/23-descriptor/bulkfood/bulkfood_v3.py[role=include]-
A primeira instância do descritor vai gerenciar o atributo
weight. -
A segunda instância do descritor vai gerenciar o atributo
price. -
O restante do corpo da classe é tão simples e limpo como o código original em bulkfood_v1.py (no [lineitem_class_v1]).
O código no bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem funciona como esperado, evitando a venda de trufas por $0:[3]
>>> truffle = LineItem('White truffle', 100, 0)
Traceback (most recent call last):
...
ValueError: value must be > 0|
Warning
|
Ao programar os métodos |
Pode ser tentador armazenar o valor de cada atributo gerenciado no próprio do descritor, mas é um erro.
Em outras palavras, seria errado armazenar o valor no self.dict:
self.__dict__[self.storage_name] = value # erradoO correto é armazenar o valor no instance.dict:
instance.__dict__[self.storage_name] = value # certoPara entender por que isso está errado, pense no significado dos dois primeiros
argumentos passados a __set__: self e instance. Aqui, self é a
instância do descritor, que na verdade é um atributo de classe da classe
gerenciada. Você pode ter milhares de instâncias de LineItem na memória em um
dado momento, mas terá apenas duas instâncias dos descritores: os atributos de
classe LineItem.weight e LineItem.price. Então, qualquer coisa armazenada
nas próprias instâncias do descritor é na verdade parte de um atributo de classe
de LineItem, e portanto é compartilhada por todas as instâncias de LineItem.
Um inconveniente do bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem é a necessidade de repetir os nomes dos atributos quando os descritores são instanciados no corpo da classe gerenciada. Seria bom se a classe LineItem pudesse ser declarada assim:
class LineItem:
weight = Quantity()
price = Quantity()
# o restante dos métodos permanece igualDa forma como está escrito, o bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem exige nomear explicitamente cada Quantity, algo não apenas inconveniente, mas também perigoso. Se um programador, ao copiar e colar código, se esquecer de editar os dois nomes, e terminar com uma linha como price = Quantity('weight'), o programa vai se comportar de forma muito errática, sobrescrevendo o valor de weight sempre que price for definido.
O problema é que—como vimos no [ch_refs_mut_mem]—o lado direito de uma atribuição é executado antes da variável existir. A expressão Quantity() é avaliada para criar uma instância do descritor, e não há como o código na classe Quantity adivinhar o nome da variável à qual o descritor será vinculado (por exemplo, weight ou price).
Felizmente, o protocolo de descritor agora suporta o método __set_name__. Veremos a seguir como usá-lo.
|
Note
|
Nomear automaticamente o atributo de armazenamento de um descritor era uma tarefa espinhosa. Na primeira edição do Python Fluente, dediquei várias páginas e muitas linhas de código neste capítulo e no seguinte para apresentar diferentes soluções, incluindo o uso de um decorador de classe e depois metaclasses (no [ch_class_metaprog]). Tudo isso ficou mais simples no Python 3.6. |
Para não ter que repetir o nome do atributo ao criar uma instância de descritor, vamos implementar
__set_name__, para definir o storage_name de cada instância de Quantity. O método especial
__set_name__ foi acrescentado ao protocolo de descritor no Python 3.6.
O interpretador invoca __set_name__ em cada descritor encontrado no corpo de uma class—se o descritor implementar esse método.[4]
No bulkfood_v4.py: __set_name__ define o nome para cada instância do descritor Quantity, a classe descritora Quantity não precisa de um __init__.
Em vez disso, __set_item__ armazena o nome do atributo de armazenamento.
__set_name__ define o nome para cada instância do descritor Quantitylink:../code/23-descriptor/bulkfood/bulkfood_v4.py[role=include]-
selfé a instância do descritor (não a instância gerenciada),owneré a classe gerenciada enameé o nome do atributo deownerao qual essa instância do descritor foi atribuída no corpo da classe deowner. -
Isso é o que o
__init__fazia no bulkfood_v3.py: o descritorQuantitynão aceita valores negativos. -
O método
__set__aqui é exatamente igual ao do bulkfood_v3.py: o descritorQuantitynão aceita valores negativos. -
Não é necessário implementar
__get__, porque o nome do atributo de armazenamento é igual ao nome do atributo gerenciado. A expressãoproduct.priceobtém o atributopricediretamente da instância deLineItem. -
Não é necessário passar o nome do atributo gerenciado para o construtor de
Quantity. Esse era o objetivo dessa versão.
Olhando para o bulkfood_v4.py: __set_name__ define o nome para cada instância do descritor Quantity, pode parecer muito código só para gerenciar um par de atributos,
mas é importante perceber que agora abstraímos a lógica do descritor em uma unidade de código separada e reutilizável: a classe Quantity.
Normalmente não definimos um descritor no mesmo módulo em que ele é usado,
mas em um módulo utilitário separado,
para ser usado por toda a aplicação—ou mesmo por muitas aplicações,
se estivermos desenvolvendo uma biblioteca ou um framework.
Tendo isso em mente, o bulkfood_v4c.py: uma definição mais limpa de LineItem; a classe descritora Quantity agora reside no módulo importado model_v4c representa melhor o uso típico de um descritor.
LineItem; a classe descritora Quantity agora reside no módulo importado model_v4clink:../code/23-descriptor/bulkfood/bulkfood_v4c.py[role=include]-
Importa o módulo
model_v4c, ondeQuantityé implementada. -
Coloca
model.Quantityem uso.
Usuários do Django vão perceber que o bulkfood_v4c.py: uma definição mais limpa de LineItem; a classe descritora Quantity agora reside no módulo importado model_v4c se parece muito com uma definição de modelo. Isso não é uma coincidência: os campos de modelos Django são descritores.
Já que descritores são implementados como classes, podemos aproveitar a herança para reutilizar parte do código que já temos em novos descritores. É o que faremos na próxima seção.
A loja imaginária de comida orgânica encontra um problema: de alguma forma, uma
instância de um produto foi criada com uma descrição vazia, e o pedido não pode
ser processado. Para prevenir isso, criaremos um novo descritor: NonBlank. Ao
projetar NonBlank, percebemos que ele será muito parecido com o descritor
Quantity, exceto pela lógica de validação.
Isto sugere uma refatoração, resultando em Validated, uma classe abstrata que sobrescreve um método
__set__, invocando o método validate, que precisa ser implementado por subclasses.
Vamos então reescrever Quantity e implementar NonBlank, herdando de Validated e programando apenas os métodos validate.
A relação entre Validated, Quantity e NonBlank é uma aplicação do padrão Template Method (Método Gabarito),
como descrito no clássico Design Patterns:
O Método Gabarito define um algoritmo em termos de operações abstratas que subclasses sobrescrevem para fornecer o comportamento concreto.[5]
No model_v5.py: the Validated ABC, Validated.__set__ é um método gabarito e self.validate é a operação abstrata.
Validated ABClink:../code/23-descriptor/bulkfood/model_v5.py[role=include]-
__set__delega a validação para o métodovalidate… -
…e então usa o
valuedevolvido para atualizar o valor armazenado. -
validateé um método abstrato; este é o método que "preenche" gabarito__set__, definindo parte do seu comportamento.
Alex Martelli prefere chamar este padrão de projeto Self-Delegation (Auto-Delegação),
e concordo que é um nome mais descritivo: a primeira linha de __set__ auto-delega para
validate, ou seja, invoca outro método na mesma instância de uma subclasse concreta de Validated.[6]
As subclasses concretas de Validated neste exemplo são Quantity e NonBlank, apresentadas no model_v5.py: Quantity e NonBlank, subclasses concretas de Validated.
Quantity e NonBlank, subclasses concretas de Validatedlink:../code/23-descriptor/bulkfood/model_v5.py[role=include]-
Implementação exigida pelo método abstrato
Validated.validate. -
Se não sobrar nada após a remoção dos espaços em branco antes e depois do valor, este é rejeitado.
-
Exigir que os métodos
validateconcretos devolvam o valor validado dá a eles a oportunidade de limpar, converter ou normalizar os dados recebidos. Neste caso,valueé devolvido sem espaços iniciais ou finais.
Usuários de model_v5.py não precisam saber todos esses detalhes. O que importa é poder usar Quantity e NonBlank para automatizar a validação de atributos de instância. Veja a última classe LineItem no bulkfood_v5.py: LineItem usando os descritores Quantity e NonBlank.
LineItem usando os descritores Quantity e NonBlanklink:../code/23-descriptor/bulkfood/bulkfood_v5.py[role=include]-
Importa o módulo
model_v5, dando a ele um nome amigável. -
Usa
model.NonBlank. O restante do código não foi modificado.
Os exemplos de LineItem que vimos neste capítulo demonstram um uso típico de
descritores para gerenciar atributos de dados. Descritores como Quantity são
chamados descritores dominantes, pois seu método __set__ sobrescreve (isto é,
intercepta e impede) a atribuição de um atributo de instância com o mesmo nome na
instância gerenciada. Entretanto, há também descritores não dominantes. Vamos
explorar essa diferença detalhadamente na próxima seção.
Recordando, há uma importante assimetria na forma como Python lida com atributos. Em uma classe sem descritores, ler um atributo através de uma instância devolve o atributo definido na instância. Mas se tal atributo não existir na instância, um atributo de classe será obtido. Por outro lado, uma atribuição a um atributo em uma instância cria o atributo na instância, sem afetar a classe de forma alguma.
Esta assimetria também afeta descritores, criando duas grandes categorias de descritores, dependendo do método __set__ estar ou não implementado.
Se __set__ estiver presente, a classe é um descritor dominante (overriding descriptor); caso contrário, ela é um descritor não dominante
(non-overriding descriptor).
Esses termos farão sentido quando examinarmos os comportamentos de descritores nos próximos exemplos.
Usei algumas funções auxiliares definidas no descriptorkinds.py: funções auxiliares para formatar e exibir objetos para observar as diferentes categorias de descritores,
Sua lógica não é importante, mas elas são usadas nas invocações a print_args no descriptorkinds.py: classes simples para estudar os comportamentos dominantes de descritores.
link:../code/23-descriptor/descriptorkinds.py[role=include]Agora vamos criar três classes de descritores, e uma classe Managed onde os descritores são instalados.
link:../code/23-descriptor/descriptorkinds.py[role=include]-
Uma classe descritora dominante com
__get__e__set__. -
A função
print_argsé chamada por todos os métodos do descritor neste exemplo. -
Um descritor dominante sem um método
__get__. -
Nenhum método
__set__aqui, então este é um descritor não dominante. -
A classe gerenciada, usando uma instância de cada uma das classes descritoras.
-
O método
spamestá aqui para efeito de comparação, pois métodos também são descritores.
Nas próximas seções, examinaremos o comportamento de leitura e escrita de atributos na classe Managed e em uma de suas instâncias, passando por cada um dos diferentes descritores definidos.
Um descritor que implementa o método __set__ é um descritor dominante pois, apesar de ser um atributo de classe, um descritor que implementa __set__ irá sobrescrever tentativas de atribuição a atributos de instância. É assim que o bulkfood_v4.py: __set_name__ define o nome para cada instância do descritor Quantity foi implementado. Propriedades também são descritores dominantes: se você não fornecer uma função setter, o __set__ default da classe property vai gerar um AttributeError, para sinalizar que o atributo é somente para leitura.
|
Warning
|
Contribuidores e autores da comunidade Python usam termos diferentes ao discutir esses conceitos. Adotei "descritor dominante" (overriding descriptor), do livro Python in a Nutshell. A documentação oficial de Python usa "descritor de dados" (data descriptor) mas "descritor dominante" destaca o comportamento especial. Descritores dominantes também são chamados "descritores forçados" (enforced descriptors). Sinônimos para descritores não dominantes incluem "descritores sem dados" (nondata descriptors, na documentação oficial em português) ou "descritores ocultáveis" (shadowable descriptors). |
Dado o código no descriptorkinds.py: classes simples para estudar os comportamentos dominantes de descritores, alguns experimentos com um descritor dominante podem ser vistos no O comportamento de um descritor dominante.
link:../code/23-descriptor/descriptorkinds.py[role=include]-
Cria o objeto
Managed, para testes. -
obj.overaciona o método__get__do descritor, passando a instância gerenciadaobjcomo segundo argumento. -
Managed.overaciona o método__get__do descritor, passandoNonecomo segundo argumento (instance). -
Atribuir a
obj.overaciona o método__set__do descritor, passando o valor7como último argumento. -
Ler
obj.overainda invoca o método__get__do descritor. -
Contorna o descritor, definindo um valor diretamente no
obj.__dict__. -
Verifica se aquele valor está no
obj.__dict__, sob a chaveover. -
Entretanto, mesmo com um atributo de instância chamado
over, o descritorManaged.overcontinua interceptando tentativas de lerobj.over.
Propriedades e outros descritores dominantes, tal como os campos de modelo do Django, implementam tanto __set__ quanto __get__. Mas também é possível implementar apenas __set__, como vimos no bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem. Neste caso, apenas a escrita é controlada pelo descritor. Ler o descritor através de uma instância irá devolver o próprio objeto descritor, pois não há um
__get__ para tratar daquele acesso. Se um atributo de instância de mesmo nome for criado com um novo valor, através de acesso direto ao __dict__ da instância, o método __set__ continuará interceptando tentativas posteriores de definir aquele atributo, mas a leitura do atributo vai simplesmente devolver o novo valor na instância, em vez de devolver o objeto descritor. Em outras palavras, o atributo de instância vai ocultar o descritor, mas apenas na leitura. Veja o Descritor dominante sem __get__.
__get__link:../code/23-descriptor/descriptorkinds.py[role=include]-
Este descritor dominante não tem um método
__get__, então lerobj.over_no_getobtém a instância do descritor a partir da classe. -
A mesma coisa acontece se obtivermos a instância do descritor diretamente da classe gerenciada.
-
Tentar definir um valor para
obj.over_no_getinvoca o método__set__do descritor. -
Como nosso
__set__não faz modificações, lerobj.over_no_getobtém a instância do descritor na classe gerenciada. -
Definindo um atributo de instância chamado
over_no_getdireto no__dict__da instância. -
Agora aquele atributo de instância
over_no_getoculta o descritor, mas só na leitura. -
Tentar atribuir um valor a
obj.over_no_getcontinua passando pelo__set__do descritor. -
Mas na leitura, aquele descritor é ocultado enquanto existir um atributo de instância de mesmo nome.
Um descritor que não implementa __set__ é um descritor não dominante. Definir um atributo de instância com o mesmo nome vai ocultar o descritor, tornando-o incapaz de tratar aquele atributo naquela instância específica. Métodos e a @functools.cached_property são implementados como descritores não dominantes. O Comportamento de um descritor não dominante mostra o comportamento de um descritor não dominante.
link:../code/23-descriptor/descriptorkinds.py[role=include]-
obj.non_overaciona o método__get__do descritor, passandoobjcomo segundo argumento. -
Managed.non_overé um descritor não dominante, então não há um__set__para interferir com essa atribuição. -
O
objagora tem um atributo de instância chamadonon_over, que oculta o atributo do descritor de mesmo nome na classeManaged. -
O descritor
Managed.non_overainda está lá, e intercepta esse acesso através da classe. -
Se o atributo de instância
non_overfor excluído… -
…então ler
obj.non_overencontra o método__get__do descritor; mas observe que o segundo argumento é a instância gerenciada.
Nos exemplos anteriores, vimos várias atribuições a um atributo de instância com nome igual ao do descritor, com resultados diferentes dependendo da presença ou não de um método __set__ no descritor.
A definição de atributos na classe não pode ser controlada por descritores ligados à mesma classe. Em especial, isso significa que os próprios atributos do descritor podem ser danificados por atribuições à classe, como explicado na próxima seção.
Independente de o descritor ser dominante ou não, ele pode ser sobrescrito por uma atribuição à classe. Isso é uma técnica de monkey-patching mas, no Qualquer descritor pode ser sobrescrito na própria classe, os descritores são substituídos por números inteiros, algo que certamente quebraria a lógica de qualquer classe que dependesse dos descritores para seu funcionamento correto.
link:../code/23-descriptor/descriptorkinds.py[role=include]-
Cria uma nova instância para testes posteriores.
-
Sobrescreve os atributos dos descritores na classe.
-
Os descritores realmente desapareceram.
O Qualquer descritor pode ser sobrescrito na própria classe expõe outra assimetria entre a leitura e a escrita de atributos: apesar da leitura de um atributo de classe poder ser controlada por um __get__ de um descritor ligado à classe gerenciada, a escrita em um atributo de classe não pode ser tratada por um __set__ de um descritor ligado à mesma classe.
|
Tip
|
Para controlar a escrita a atributos em uma classe, é preciso associar descritores à classe da classe—em outras palavras, à metaclasse. Por default, a metaclasse de classes definidas pelo usuário é |
Vamos ver agora como descritores são usados para implementar métodos no Python.
Uma
função dentro de uma classe se torna um método vinculado (bound method) quando invocada em uma
instância, porque todas as funções definidas pelo usuário têm um método
__get__, e portanto operam como descritores quando associados a uma classe.
O Um método é um descritor não dominante demonstra a leitura do método spam, da classe
Managed, apresentada no descriptorkinds.py: classes simples para estudar os comportamentos dominantes de descritores.
link:../code/23-descriptor/descriptorkinds.py[role=include]-
Ler de
obj.spamobtém um objeto método vinculado. -
Mas ler de
Managed.spamobtém uma função. -
Atribuir um valor a
obj.spamoculta o atributo de classe, tornando o métodospaminacessível a partir da instânciaobj.
Funções não implementam __set__, portanto não são descritores dominantes, como mostra a última linha do Um método é um descritor não dominante.
A outra lição fundamental do Um método é um descritor não dominante é que obj.spam e Managed.spam devolvem objetos diferentes. Como de hábito com descritores, o __get__ de uma função devolve uma referência para a própria função quando o acesso ocorre através da classe gerenciada. Mas quando o acesso vem através da instância, o __get__ da função devolve um método vinculado: um invocável que embrulha a função e vincula a instância gerenciada (no exemplo, obj) ao primeiro argumento da função (isto é, self), como faz a função functools.partial (que vimos na [functools_partial_sec]).
Para um entendimento mais profundo deste mecanismo, dê uma olhada no method_is_descriptor.py: uma classe Text, derivada de UserString.
Text, derivada de UserStringlink:../code/23-descriptor/method_is_descriptor.py[role=include]Vamos então investigar o método Text.reverse. Veja o Experimentos com um método.
link:../code/23-descriptor/method_is_descriptor.py[role=include]-
Seguindo a convenção, o
reprde uma instância deTextparece uma chamada ao construtor deTextque criaria uma instância igual. -
O método
reversedevolve o texto escrito de trás para frente. -
Um método invocado na classe funciona como uma função.
-
Observe os tipos diferentes:
functionemethod. -
Text.reverseopera como uma função, mesmo ao trabalhar com objetos que não são instâncias deText. -
Toda função é um descritor não dominante. Invocar seu
__get__com uma instância obtém um método vinculado àquela instância. -
Invocar o
__get__da função comNonecomo argumentoinstanceobtém a própria função. -
A expressão
word.reverseinvocaText.reverse.__get__(word), devolvendo o método vinculado, mas sem invocá-lo. -
O objeto método vinculado tem um atributo
__self__, contendo uma referência à instância na qual o método foi invocado. -
O atributo
__func__do método vinculado é uma referência à função original, implementada na classe gerenciada.
O método vinculado contém um método __call__, que trata a invocação em si. Este método chama a função original, referenciada em __func__, passando o atributo __self__ do método como primeiro argumento. É assim que funciona a vinculação implícita do argumento self convencional.
A conversão de funções em métodos vinculados é um exemplo perfeito de como descritores são usados na infraestrutura da linguagem.
Após este mergulho profundo no funcionamento de descritores e métodos, vamos repassar alguns conselhos práticos sobre seu uso.
A lista a seguir trata de algumas consequências práticas das características dos descritores descritas acima:
- Use
propertypara não complicar demais -
A classe embutida
propertycria descritores dominantes, implementando__set__e__get__, mesmo se um método setter não for definido.[7] O__set__default de uma propriedade gera umAttributeError: can’t set attribute(não é permitido setar o atributo), então uma propriedade é a forma mais fácil de criar um atributo somente para leitura, evitando o problema descrito a seguir. - Descritores somente para leitura exigem um
__set__ -
Se você usar uma classe descritora para implementar um atributo somente para leitura, precisa lembrar de programar tanto
__get__quanto__set__. Caso contrário, você terá um descritor não-dominante, e setar um atributo com o mesmo nome em uma instância vai ocultar o descritor. O método__set__de um atributo somente para leitura deve apenas levantarAttributeErrorcom uma mensagem adequada.[8] - Descritores de validação podem funcionar apenas com
__set__ -
Em um descritor projetado apenas para validação, o método
__set__deve verificar o argumentovaluerecebido e, se ele for válido, atualizar o__dict__da instância diretamente, usando o nome da instância do descritor como chave. Dessa forma, ler o atributo de mesmo nome a partir da instância será tão rápido quanto possível, pois não vai precisar de um__get__. Veja o código no bulkfood_v4.py:__set_name__define o nome para cada instância do descritorQuantity. - Caching pode ser feito eficientemente apenas com
__get__ -
Se você escrever apenas o método
__get__, cria um descritor não dominante. Eles são úteis para executar alguma computação custosa e então armazenar o resultado, definindo um atributo com o mesmo nome na instância[9]. O atributo de mesmo nome na instância vai ocultar o descritor, daí acessos subsequentes àquele atributo vão buscá-lo diretamente no__dict__da instância, sem acionar mais o__get__do descritor. O decorador@functools.cached_propertyconstrói um descritor não dominante. - Métodos não especiais podem ser ocultados por atributos de instância
-
Como funções e métodos implementam apenas
__get__, eles são descritores não dominantes. Uma atribuição simples, comomeu_obj.o_método = 7, significa que acessos posteriores ao_métodoatravés daquela instância irão obter o número 7—sem afetar a classe ou outras instâncias. Isto se aplica aos métodos especiais. O interpretador só acessa métodos especiais na própria classe. Em outras palavras,repr(x)é executado comox.__class__.__repr__(x), então um atributo__repr__, definido emx, não tem efeito emrepr(x). Pela mesma razão, a existência de um atributo chamado__getattr__em uma instância não vai subverter o algoritmo normal de acesso a atributos.
O fato de métodos poderem ser sobrescritos tão facilmente pode soar frágil e propenso a erros. Mas eu, pessoalmente, em mais de 20 anos programando em Python, nunca tive problemas com isso. Por outro lado, se você estiver criando muitos atributos dinâmicos, onde os nomes dos atributos vêm de dados que você não controla (como fizemos na parte inicial desse capítulo), então você precisa estar atenta para isso, e talvez implementar alguma filtragem ou reescrita (escaping) dos nomes dos atributos dinâmicos, para preservar sua sanidade.
|
Note
|
A classe |
Para encerrar esse capítulo, vamos falar de dois recursos que vimos com as propriedades, mas não no contexto dos descritores: documentação e o tratamento de tentativas de excluir um atributo gerenciado.
A docstring de uma classe descritora é usada para documentar todas as instâncias do descritor na classe gerenciada.
A Capturas de tela do console de Python após os comandos help(LineItem.weight) e help(LineItem). mostra as telas de ajuda para a classe LineItem com os descritores Quantity e NonBlank, do
model_v5.py: Quantity e NonBlank, subclasses concretas de Validated e do bulkfood_v5.py: LineItem usando os descritores Quantity e NonBlank.
Isso é um tanto insatisfatório. No caso de LineItem, seria bom acrescentar, por exemplo, a informação de que weight deve ser expresso em quilogramas. Isso seria trivial com propriedades, pois cada propriedade controla um atributo gerenciado específico. Mas com descritores, a mesma classe descritora Quantity é usada para weight e price.[10]
O segundo detalhe que discutimos com propriedades, mas não com descritores, é o
tratamento de tentativas de apagar um atributo gerenciado. Isso pode ser feito
implementando um método __delete__ na classe descritora. Omiti
deliberadamente falar de __delete__, porque seu uso no mundo
real é raro. Se você precisar disso, por favor consulte a seção
«Implementando descritores» na documentação do
Modelo de dados de Python. Escrever um exemplo com uma classe descritora
boba com __delete__ fica como exercício para o leitor com excesso de tempo livre.
O primeiro exemplo deste capítulo foi uma continuação dos exemplos LineItem do [ch_dynamic_attrs]. No bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem, substituímos propriedades por descritores. Vimos que um descritor é uma classe que fornece instâncias instaladas como atributos na classe gerenciada. Discutir esse mecanismo exigiu uma terminologia especial, apresentando termos como instância gerenciada e atributo de armazenamento.
Na LineItem versão #4: Nomeando atributos de armazenamento automaticamente, removemos a exigência de descritores Quantity serem
instanciados com um storage_name explícito. A solução foi implementar o método especial __set_name__ em
Quantity, para preservar o nome da propriedade gerenciada como
self.storage_name.
A LineItem versão #5: um novo tipo descritor mostrou como criar uma subclasse de uma classe descritora abstrata, para compartilhar código ao programar descritores especializados com alguma funcionalidade em comum.
Examinamos então os comportamentos diferentes de descritores, fornecendo ou omitindo o método
__set__, criando uma distinção fundamental entre descritores dominantes e não dominantes. Por meio de testes detalhados, revelamos quando os descritores estão no controle, e quando são ocultados, contornados ou sobrescritos.
Em seguida, estudamos uma categoria específica de descritores não dominantes: métodos. Experimentos no console revelaram como uma função associada a uma classe se torna um método ao ser acessada através de uma instância, graças ao protocolo de descritores.
Para concluir o capítulo, a Dicas para usar descritores trouxe dicas práticas, e a Docstrings e exclusão de descritores forneceu um rápido olhar sobre como documentar descritores.
|
Note
|
Como observado na Novidades neste capítulo, vários exemplos deste capítulo se tornaram mais simples graças ao método especial |
Além da referência obrigatória ao capítulo «Modelo de dados», o «Guia de descritores», de Raymond Hettinger, é um recurso valioso-e parte da excelente «coleção de HOWTOS» na documentação oficial de Python.
Como sempre, em se tratando de assuntos relativos ao modelo de objetos de Python, o Python in a Nutshell, 3ª ed. (O’Reilly), de Martelli, Ravenscroft, e Holden é competente e objetivo. Martelli também tem uma apresentação chamada Python’s Object Model (O Modelo de Objetos do Python), tratando com profundidade de propriedades e descritores: «slides» e «video».
|
Warning
|
Cuidado, qualquer tratamento de descritores escrito ou gravado antes da PEP 487 ser adotada, em 2016, corre o risco de conter exemplos desnecessariamente complicados hoje, pois |
Para mais exemplos práticos, o Python Cookbook, 3ª ed., de David Beazley e
Brian K. Jones (O’Reilly), traz muitas receitas ilustrando descritores, entre
as quais destaco
6.12. Reading Nested and Variable-Sized Binary Structures
(Lendo Estruturas Binárias Aninhadas e de Tamanho Variável),
8.10. Using Lazily Computed Properties
(Usando Propriedades Computadas de Forma Preguiçosa),
8.13. Implementing a Data Model or Type System
(Implementando um Modelo de Dados ou um Sistema de Tipos) e
9.9. Defining Decorators As Classes
(Definindo Decoradores como Classes).
Essa última receita trata das questões profundas envolvidas na interação entre
decoradores de função, descritores e métodos, e de como um decorador de função
implementado como uma classe, com __call__, também precisa implementar
__get__ se quiser funcionar com métodos.
A PEP 487—Simpler customization of class creation
(Customização simplificada da criação de classes)
introduziu o método especial __set_name__ e inclui um exemplo de um
«descritor de validação».
O design do parâmetro self
A exigência de declarar
self explicitamente como o primeiro parâmetro em métodos foi uma decisão de
design controversa no Python. Eu me acostumei em menos de 20 anos!
Acho que essa decisão é um exemplo de "pior é melhor"
(worse is better): a filosofia de design descrita pelo cientista da
computação Richard P. Gabriel em
The Rise of Worse is Better
(A Ascensão do Pior é Melhor). A primeira prioridade dessa filosofia
é "simplicidade", que Gabriel apresenta assim:
O design deve ser simples, tanto na implementação quanto na interface. É mais importante simplificar a implementação do que a interface. A simplicidade é a consideração mais importante em um design.
O self explícito de Python incorpora esta filosofia de design.
A implementação é simples—até mesmo elegante—em prejuízo da usabilidade:
uma assinatura de método como def zfill(self, width): não corresponde, visualmente, à invocação label.zfill(8).
A linguagem Modula-3, que Guido estudou antes de inventar o Python, introduziu esta convenção com o mesmo identificador, self.
Mas há uma diferença crucial: em Modula-3, interfaces são declaradas separadamente de sua implementação,
e na declaração da interface o argumento self é omitido.
Então, da perspectiva do usuário, um método aparece em uma declaração de interface com a mesma quantidade de parâmetros necessários para invocá-lo.
Ao longo do tempo, as mensagens de erro de Python relacionadas a argumentos de métodos se tornaram mais claras.
Em um método definido pelo usuário com um argumento além de self, se o usuário invocasse
obj.meth(), Python 2.7 gerava:
TypeError: meth() takes exactly 2 arguments (1 given)
(meth() recebe exatamente 2 argumentos (1 passado))No Python 3, a confusa contagem de argumentos não é mencionada, mas o argumento ausente é nomeado:
TypeError: meth() missing 1 required positional argument: 'x'
(1 argumento posicional obrigatório faltando em meth(): 'x')Além do uso de self como um argumento explícito, a exigência de qualificar
cada acesso a atributos de instância com self também é criticada. Veja, por
exemplo, o famoso post Python Warts (Verrugas de Python) de A. M.
Kuchling («cópia arquivada» no Internet Archive);
o próprio Kuchling não se incomoda
muito com o qualificador self, mas ele o menciona—provavelmente ecoando
opiniões do grupo comp.lang.python. Pessoalmente não me importo em digitar o
qualificador self: é bom para distinguir variáveis locais de atributos. Minha
questão é com o uso de self na instrução def.
Quem estiver triste com o self explícito de Python pode se sentir bem melhor após considerar a
«semântica desconcertante» do this implícito em JavaScript.
Guido teve boas razões para fazer self funcionar como funciona, e ele escreveu sobre elas em
Adding Support for User-Defined Classes
(Adicionando Suporte a Classes Definidas pelo Usuário),
em seu blog, The History of Python (A História de Python).
__set_name__ é invocado por type.__new__—o construtor de objetos que representam classes. A classe embutida type é na verdade uma metaclasse, a classe default de classes definidas pelo usuário. Isso é um pouco difícil de entender de início, mas fique tranquila: o [ch_class_metaprog] é dedicado à configuração dinâmica de classes, incluindo o conceito de metaclasses.
__delete__ também é fornecido pelo decorador property, mesmo se você não definir um método deleter (de exclusão).
c.real de um número complex resulta em um AttributeError: readonly attribute (atributo somente para leitura), mas uma tentativa de mudar c.conjugate (um método de complex) levanta um AttributeError: 'complex' object attribute 'conjugate' is read-only (o atributo 'conjugate' do objeto 'complex' é somente para leitura). Até "read-only" está escrito de maneira diferente nas mensagens em inglês).
__init__ frustra a otimização de memória através de compartilhamento de chaves, como discutido na [conseq_dict_internal_sec].



