Aprender sobre descritores não apenas dá acesso a um conjunto maior de ferramentas, cria também uma maior compreensão sobre o funcionamento do Python e uma apreciação pela elegância de seu design.[1]
guru do Python e um de seus desenvolvedores principais
Descritores são uma forma de reutilizar a mesma lógica de acesso em múltiplos atributos. Por exemplo, tipos de campos em ORMs ("Object Relational Mapping" - Mapeamento Objeto-Relacional), tais como o ORM do Django e o SQLAlchemy, são descritores, gerenciando o fluxo de dados dos campos em um registro de banco de dados para atributos de objetos do Python, e vice-versa.
Um descritor é uma classe que implementa um protocolo dinâmico, composto pelos métodos __get__
, __set__
, e __delete__
. A classe property
implementa o protocolo 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 implementam apenas
__get__
e __set__
, e muitos implementam apenas um destes métodos.
Descritores são um recurso característico do 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 descritor permite que métodos operem como métodos vinculados ou desvinculados, dependendo de como são invocados.
Entender os descritores é crucial para dominar o Python. Esse capítulo é sobre isso.
Nas próximas páginas vamos refatorar o exemplo da loja de comida orgânica a granel, visto na [prop_validation_sec], substituindo propriedades por descritores. Isso 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 do 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, foi dramaticamente simplificado, graças ao método especial __set_name__
, adicionado ao protocolo descritor no Python 3.6. Nessa 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__
a solução com o descritor se tornou muito mais simples.
A classe AutoStorage
, que aparecia na LineItem versão #5: um novo tipo descritor, também foi removida, pois o mesmo __set_name__
a tornou obsoleta.
Como vimos na [coding_prop_factory_sec], uma fábrica de propriedades é uma maneira de evitar código repetitivo de getters e setters, aplicando padrões de programação funcional.
Um 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 personalizada, com clausuras para manter 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. Podemos usar um descritor declarando instâncias dele como atributos de classe 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 Figura 1.
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
na classe LineItem
, mas instâncias de LineItem
também têm seus próprios atributos weight
e price
, onde esses valores são armazenados.Note que a palavra weight
aparece duas vezes na Figura 1, 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
-
Uma classe que implementa o protocolo descritor. Por exemplo,
Quantity
na Figura 1. - Classe gerenciada
-
A classe onde as instâncias do descritor são declaradas, como atributos de classe. Na Figura 1,
LineItem
é a classe gerenciada. - Instância do descritor
-
Cada instância de uma classe descritora, declarada como um atributo de classe da classe gerenciada. Na Figura 1, 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 classe
LineItem
, que contém as instâncias do descritor. - Instância gerenciada
-
Uma instância da classe gerenciada. Nesse exemplo, instâncias de
LineItem
são as instâncias gerenciadas (elas não aparecem no diagrama de classe). - Atributo de armazenamento
-
Um atributo da instância gerenciada que mantém o valor de um atributo gerenciado para aquela instância específica. Na Figura 1, os atributos de instância
weight
eprice
deLineItem
são atributos de armazenamento. Eles são diferentes das instâncias do descritor, que são sempre atributos de classe. - Atributos gerenciados
-
Um atributo público na classe gerenciada que é controlado por uma instância do descritor, com os valores mantidos em atributos de armazenamento. Em outras palavras, 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 Figura 2.
Quantity
produz duas bugigangas de cabeça redonda, que são anexadas à engenhoca LineItem
: weight
e price
. A engenhoca LineItem
produz bugigangas retangulares que tem 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.
A MGN é projetada para tornar bastante clara a diferença entre classes e instâncias. Veja a Figura 3. Na MGN, uma classe aparece como uma "engenhoca", uma máquina complexa que produz bugigangas. Classes/engenhocas são sempre máquina com alavancas e mostradores. As bugigangas são as instâncias, e elas têm uma aparência bem mais simples. Quando este livro é gerado em cores, as bugigangas tem 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, eles tem uma lente de aumento para __get__
(obter) os valores, e uma garra para __set__
(definir) os valores. Quando chegarmos às metaclasses, você me agradecerá por esses desenhos.
Mas chega de rabiscos por enquanto. Aqui está o código: o Exemplo 1 mostra a classe descritora Quantity
, e o Exemplo 2 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 criar uma subclasse para implementá-lo.
-
Cada instância de
Quantity
terá um atributostorage_name
: é o nome do atributo de armazenamento que vai manter o valar nas instâncias gerenciadas. -
O
__set__
é chamado quando ocorre uma tentativa de atribuir um valor a um atributo gerenciado. Aqui,self
é a instância do descritor (isto é,LineItem.weight
ouLineItem.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__
; chamarsetattr(instance, self.storage_name)
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 argumentoowner
será explicado a seguir.
Implementar __get__
é necessário porque um usuário poderia escrever algo assim:
class House:
rooms = Quantity('number_of_rooms')
Na classe House
, o atributo gerenciado é rooms
, mas o atributo de armazenamento é number_of_rooms
.
Dada uma instância de House
chamada chaos_manor
, acessar e modificar chaos_manor.rooms
passa pela instância do descritor Quantity
ligada a rooms
, mas acessar e modificar
chaos_manor.number_of_rooms
escapa ao 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 do Python, de procurar um atributo de classe quando o nome não é encontrado na instância.
Se um atributo gerenciado, tal como weight
, é acessado através da classe como LineItem.weight
, o método __get__
do descritor recebe None
como valor do 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 Exemplo 2 demonstra o uso de Quantity
em LineItem
.
Quantity
gerenciam atributos em LineItem
link: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 orginal em bulkfood_v1.py (no [lineitem_class_v1]).
>>> 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, mas é um erro, armazenar o valor de cada atributo gerenciado na própria instância do descritor. Em outras palavras, em vez de escrever o método __set__
assim:
instance.__dict__[self.storage_name] = value
escrever a alternativa tentadora mas ruim, assim:
self.__dict__[self.storage_name] = value
Para entender porque 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 Exemplo 2 é 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 igual
Da forma como está escrito, o Exemplo 2 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 [mutability_and_references]—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 descritor agora suporta o muito bem batizado método __set_name__
. Veremos a seguir como usá-lo.
Note
|
Nomear automaticamente o atributo de armazenamendo de um descritor contumava ser 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 [class_metaprog]). Tudo isso ficou muito mais simples no Python 3.6. |
Para evitar a redigitação do nome do atributo em instâncias do 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 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 Exemplo 3, 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 Quantity
link: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 deowner
ao qual essa instância do descritor foi atrbuída no corpo da classe deowner
. -
Isso é o que o
__init__
fazia no Exemplo 1. -
O método
__set__
aqui é exatamente igual ao do Exemplo 1. -
Não é necessário implementar
__get__
, porque o nome do atributo de armazenamento é igual ao nome do atributo gerenciado. A expressãoproduct.price
obtém o atributoprice
diretamente 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 Exemplo 3, pode parecer muito código apenas para gerenciar um par de atributos, mas é importante perceber que a lógica do descritor foi agora abstraida em uma unidade de código diferente: a classe Quantity
.
Nós normalmente sequer definimos um descritor no mesmo módulo em que ele é usado, mas em um módulo utilitário separado, projetado para ser usado por toda a aplicação—ou mesmo por muitas aplicações, se estivermos desenvolvendo uma bliblioteca ou um framework.
Tendo isso em mente, o Exemplo 4 representa melhor o uso típico de um descritor.
LineItem
; a classe descritora Quantity
agora reside no módulo importado model_v4c
link:code/23-descriptor/bulkfood/bulkfood_v4c.py[role=include]
-
Importa o módulo
model_v4c
, ondeQuantity
é implementada. -
Coloca
model.Quantity
em uso.
Usuários do Django vão perceber que o Exemplo 4 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 implementado 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 obstáculo: 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.
Isso leva a uma refatoração, resultando em Validated
, uma classe abstrata que sobrepõe 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 método modelo ("template method"), como descrito no clássico Design Patterns:
Um método modelo define um algoritimo em termos de operações abstratas que subclasses sobrepõe para fornecer o comportamento concreto.[5]
No Exemplo 5, Validated.__set__
é um método modelo 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
value
devolvido para atualizar o valor armazenado. -
validate
é um método abstrato; este é o método modelo.
Alex Martelli prefere chamar este padrão de projeto Auto-Delegação ("Self-Delegation"),
e concordo que é um nome mais descritivo: a primeira linha de __set__
auto-delega para
validate
.[6]
As subclasses concretas de Validated
neste exemplo são Quantity
e NonBlank
, apresentadas no Exemplo 6.
Quantity
e NonBlank
, subclasses concretas de Validated
link:code/23-descriptor/bulkfood/model_v5.py[role=include]
-
Implementação do método modelo exigida pelo método abstrado
Validated.validate
. -
Se não sobrar nada após a remoção os espaços em branco antes e depois do valor, este é rejeitado.
-
Exigir que os métodos
validate
concretos 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 Exemplo 7.
LineItem
usando os descritores Quantity
e NonBlank
link: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 chamado descritores dominantes, pois seu método __set__
sobrepõe (isto é, intercepta e anula) a definiçã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 o Python lida com atributos. Ler um atributo através de uma instância normalmente 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 normalmente cria o atributo na instância, sem afetar a classe de forma alguma.
Essa assimetria também afeta descritores, criando efetivamente duas grandes categorias de descritores, dependendo do método __set__
estar ou não implementado.
Se __set__
estiver presente, a classe é um descritor dominante; caso contrário, ela é um descritor não dominante.
Esses termos farão sentido quando examinarmos os comportamentos de descritores, nos próximos exemplos.
Observar as categorias diferentes de descritores exige algumas classes, então vamos usar o código no Exemplo 8 como nossa bancada de testes para as próximas seções.
Tip
|
Todos os métodos |
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, estão este é um descritor não dominante. -
A classe gerenciada, usando uma instância de cada uma das classes descritoras.
-
O método
spam
está 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á sobrepor tentativas de atribuição a atributos de instância. É assim que o Exemplo 3 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 do 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 Exemplo 8, alguns experimentos com um descritor dominante podem ser vistos no Exemplo 9.
link:code/23-descriptor/descriptorkinds.py[role=include]
-
Cria o objeto
Managed
, para testes. -
obj.over
aciona o método__get__
do descritor, passando a instância gerenciadaobj
como segundo argumento. -
Managed.over
aciona o método__get__
do descritor, passandoNone
como segundo argumento (instance
). -
Atribuir a
obj.over
aciona o método__set__
do descritor, passando o valor7
como último argumento. -
Ler
obj.over
ainda 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.over
continua 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 Exemplo 2. 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 para leitura. Veja o Exemplo 10.
__get__
link:code/23-descriptor/descriptorkinds.py[role=include]
-
Este descritor dominante não tem um método
__get__
, então lerobj.over_no_get
obté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_get
invoca o método__set__
do descritor. -
Como nosso
__set__
não faz modificações, lerobj.over_no_get
novamente obtém a instância do descritor na classe gerenciada. -
Percorrendo o
__dict__
da instância para definir um atributo de instância chamadoover_no_get
. -
Agora aquele atributo de instância
over_no_get
oculta o descritor, mas apenas para leitura. -
Tentar atribuir um valor a
obj.over_no_get
continua passando pelo set do descritor. -
Mas, para 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 Exemplo 11 mostra a operação de um descritor não dominante.
link:code/23-descriptor/descriptorkinds.py[role=include]
-
obj.non_over
aciona o método__get__
do descritor, passandoobj
como segundo argumento. -
Managed.non_over
é um descritor não dominante, então não há um__set__
para interferir com essa atribuição. -
O
obj
agora tem um atributo de instância chamadonon_over
, que oculta o atributo do descritor de mesmo nome na classeManaged
. -
O descritor
Managed.non_over
ainda está lá, e intercepta esse acesso através da classe. -
Se o atributo de instância
non_over
for excluído… -
…então ler
obj.non_over
encontra 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 do descritor ser ou não dominante, ele pode ser sobrescrito por uma atribuição à classe. Isso é uma técnica de monkey-patching mas, no Exemplo 12, 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 Exemplo 12 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 tratado 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 quando invocada em uma instância, porque todas as funções definidas pelo usuário possuem um método __get__
, e portanto operam como descritores quando associados a uma classe.
O Exemplo 13 demonstra a leitura do método spam
, da classe Managed
, apresentada no Exemplo 8.
link:code/23-descriptor/descriptorkinds.py[role=include]
-
Ler de
obj.spam
obtém um objeto método vinculado. -
Mas ler de
Managed.spam
obtém uma função. -
Atribuir um valor a
obj.spam
oculta o atributo de classe, tornando o métodospam
inacessível a partir da instânciaobj
.
Funções não implementam __set__
, portanto são descritores não dominantes, como mostra a última linha do Exemplo 13.
A outra lição fundamental do Exemplo 13 é 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 objeto método vinculado: um invocável que envolve 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 desse mecanismo, dê uma olhada no Exemplo 14.
Text
, derivada de UserString
link:code/23-descriptor/method_is_descriptor.py[role=include]
Vamos então investigar o método Text.reverse
. Veja o Exemplo 15.
link:code/23-descriptor/method_is_descriptor.py[role=include]
-
O
repr
de uma instância deText
se parece com uma chamada ao construtor deText
que criaria uma instância idêntica. -
O método
reverse
devolve o texto escrito de trás para frente. -
Um método invocado na classe funciona como uma função.
-
Observe os tipos diferentes: uma
function
e ummethod
. -
Text.reverse
opera 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 a aquela instância. -
Invocar o
__get__
da função comNone
como argumentoinstance
obtém a própria função. -
A expressão
word.reverse
na verdade invocaText.reverse.__get__(word)
, devolvendo o método vinculado. -
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, ligada à classe gerenciada.
O objeto 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.
O modo como funções são transformadas em métodos vinculados é um exemplo perfeito de como descritores são usados como 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
property
para manter as coisas simples -
A classe embutida
property
cria 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
(AttributeError: não é permitido definir 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, definir 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 gerar umAttributeError
com 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 argumentovalue
recebido 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 Exemplo 3. - Caching pode ser feito de forma eficiente 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 a aquele atributo vão buscá-lo diretamente no__dict__
da instância, sem acionar mais o__get__
do descritor. O decorador@functools.cached_property
na verdade produz um descritor não dominante. - Métodos não especiais pode 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, comomy_obj.the_method = 7
, significa que acessos posteriores athe_method
através daquela instância irão obter o número 7—sem afetar a classe ou outras instâncias. Essa questão, entretanto, não interfere com os métodos especiais. O interpretador só procura 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 qualquer 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 não especiais poderem ser sobrepostos 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.
O Figura 4 mostra as telas de ajuda para a classe LineItem
com os descritores Quantity
e NonBlank
, do
Exemplo 6 e do Exemplo 7.
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 pela implementação de um método __delete__
juntamente com (ou em vez de) os habituais __get__
e/ou __set__
na classe descritora.
Omiti deliberadamente falar de __delete__
, porque acredito que 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 do Python.
Escrever um classe descritora boba com __delete__
fica como exercício para a leitora ociosa.
O primeiro exemplo deste capítulo foi uma continuação dos exemplos LineItem
do [dynamic_attributes]. No Exemplo 2, substituímos propriedades por descritores. Vimos que um descritor é uma classe que fornece instâncias, que são instaladas como atributos na classe gerenciada. Discutir esse mecanismo exigiu uma terminologia especial, apresentando termos tais 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 declarados com um storage_name
explícito, um requisito redundante e propenso a erros. A solução foi implementar o método especial __set_name__
em Quantity
, para armazenar 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, também conhecidos como descritores de dados e sem dados. 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 ao uma classe se torna um método ao ser acessada através de uma instância, se valendo do protocolo descritor.
Para concluir o capítulo, a Dicas para o uso de descritores trouxe dicas práticas, e a Docstrings de descritores e a sobreposição de exclusão forneceu um rápido olhar sobre como documentar descritores.
Note
|
Como observado na Novidades nesse capítulo, vários exemplos deste capítulo se tornaram muito mais simples graças ao método especial |
Além da referência obrigatória ao capítulo "Modelo de dados", o "HowTo - Guia de descritores", de Raymond Hettinger, é um recurso valioso—e parte da coleção de HOWTOS na documentação oficial do Python.
Como sempre, em se tratando de assuntos relativos ao modelo de objetos do 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 (veja os slides (EN) e o video (EN)).
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, dentre as quais quero destacar "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 de decoração e também com funções.
A PEP 487—Simpler customization of class creation (PEP 487—Uma personalização mais simples da criação de classes) (EN)
introduziu o método especial __set_name__
e inclui um exemplo de um
validating descriptor (descritor de validação) (EN).
O projeto de self
A exigência de declarar self
explicitamente como o primeiro argumento em métodos foi uma decisão de projeto controversa no Python.
Após 23 anos usando a linguagem, já estou acostumado com isso.
Acho que essa decisão é um exemplo de "pior é melhor" (worse is better):
a filosofia de projeto descrita pelo cientista da computação Richard P. Gabriel em
"The Rise of Worse is Better" (A Ascenção do Pior é Melhor) (EN).
A primeira prioridade dessa filosofia é "simplicidade", que Gabriel apresenta assim:
O projeto deve ser simples, tanto na implementação quanto na interface. É mais importante que a implementação seja simples, do que a interface. A simplicidade é a consideração mais importante em um projeto.
O self
explícito do Python incorpora essa filosofia de projeto.
A implementação é simples—até mesmo elegante—à custas da interface do usuário: uma assinatura de método como def zfill(self, width):
não corresponde, visualmente, à invocação label.zfill(8)
.
O Modula-3 introduziu essa convenção com o mesmo identificador, self
.
Mas há uma diferença crucial: no 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 os mesmos parâmetros explícitos usados para invocá-lo.
Ao longo do tempo, as mensagens de erro do 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()
, o Python 2.7 gerava:
TypeError: meth() takes exactly 2 arguments (1 given)
("TypeError: meth() recebe exatamente 2 argumentos (1 passado)"")
No Python 3, a confusa contagem de argumentos não é mencionada, e o argumento ausente é nomeado:
TypeError: meth() missing 1 required positional argument: 'x'
("TypeError: 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" (As verrugas do Python) de A. M. Kuchling (em archived (EN)); 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
em comandos def
.
Quem estiver triste com o self
explícito do Python pode se sentir bem melhor após considerar a
semântica desconcertante (EN) do this
implícito em JavaScript. Guido tinha algumas 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), um post em seu blog, The History of Python ("A História do 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 [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
(AttributeError: atributo somente para leitura), mas uma tentativa de mudar c.conjugate
(um método de complex
) gera um AttributeError: 'complex' object attribute 'conjugate' is read-only
(AttributeError: o atributo 'conjugate' do objeto 'complex' é somente para leitura). Até "read-only" está escrito de maneira diferente (na mensagem original em inglês).
__init__
frustra a otimização de memória através de compartilhamento de chaves, como discutido na [consequences_dict_internals].