Skip to content

Latest commit

 

History

History
667 lines (499 loc) · 53.2 KB

cap23.adoc

File metadata and controls

667 lines (499 loc) · 53.2 KB

Descritores de Atributos

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]

— Raymond Hettinger
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.

Novidades nesse capítulo

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.

Exemplo de descritor: validação de atributos

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.

LineItem versão #3: Um descritor simples

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.

Diagrama de classes UML para `Quantity` e `LineItem`
Figura 1. 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 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.

Termos para entender descritores

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 e price de LineItem 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.

Diagrama de classe UML+MGN para `Quantity` e `LineItem`
Figura 2. 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 tem seus próprios atributos weight e price, onde aqueles valores são armazenados.
Introduzindo a notação Engenhocas & Bugigangas (Mills & Gizmos)

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.

Esboço MGN para `LineItem` e `Quantity`
Figura 3. 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.

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.

Exemplo 1. bulkfood_v3.py: o descritor Quantity não aceita valores negativos
link:code/23-descriptor/bulkfood/bulkfood_v3.py[role=include]
  1. O descritor é um recurso baseado em protocolo: não é necessário criar uma subclasse para implementá-lo.

  2. Cada instância de Quantity terá um atributo storage_name: é o nome do atributo de armazenamento que vai manter o valar nas instâncias gerenciadas.

  3. 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 ou LineItem.price), instance é a instância gerenciada (uma instância de LineItem) e value é o valor que está sendo atribuído.

  4. Precisamos armazenar o valor do atributo diretamente no __dict__; chamar set​attr​(instance, self.storage_name) dispararia novamente o método __set__, levando a uma recursão infinita.

  5. Precisamos implementar __get__, pois o nome do atributo gerenciado pode não ser igual ao storage_name. O argumento owner 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 Line​Item.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.

Exemplo 2. bulkfood_v3.py: descritores Quantity gerenciam atributos em LineItem
link:code/23-descriptor/bulkfood/bulkfood_v3.py[role=include]
  1. A primeira instância do descritor vai gerenciar o atributo weight.

  2. A segunda instância do descritor vai gerenciar o atributo price.

  3. O restante do corpo da classe é tão simples e limpo como o código orginal em bulkfood_v1.py (no [lineitem_class_v1]).

O código no Exemplo 2 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 __get__ e __set__ de um descritor, tenha em mente o significado dos argumentos self e instance: self é a instância do descritor, instance é a instância gerenciada. Descritores que gerenciam atributos de instância devem armazenar os valores nas instâncias gerenciadas. É por isso que o Python fornece o argumento instance aos métodos do descritor.

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.

LineItem versão #4: Nomeando atributos de armazenamento automaticamente

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.

Exemplo 3. bulkfood_v4.py: __set_name__ define o nome para cada instância do descritor Quantity
link:code/23-descriptor/bulkfood/bulkfood_v4.py[role=include]
  1. self é a instância do descritor (não a instância gerenciada), owner é a classe gerenciada e name é o nome do atributo de owner ao qual essa instância do descritor foi atrbuída no corpo da classe de owner.

  2. Isso é o que o __init__ fazia no Exemplo 1.

  3. O método __set__ aqui é exatamente igual ao do Exemplo 1.

  4. Não é necessário implementar __get__, porque o nome do atributo de armazenamento é igual ao nome do atributo gerenciado. A expressão product.price obtém o atributo price diretamente da instância de LineItem.

  5. 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.

Exemplo 4. bulkfood_v4c.py: uma definição mais limpa de LineItem; a classe descritora Quantity agora reside no módulo importado model_v4c
link:code/23-descriptor/bulkfood/bulkfood_v4c.py[role=include]
  1. Importa o módulo model_v4c, onde Quantity é implementada.

  2. 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.

LineItem versão #5: um novo tipo descritor

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.

Exemplo 5. model_v5.py: the Validated ABC
link:code/23-descriptor/bulkfood/model_v5.py[role=include]
  1. __set__ delega a validação para o método validate…​

  2. …​e então usa o value devolvido para atualizar o valor armazenado.

  3. 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.

Exemplo 6. model_v5.py: Quantity e NonBlank, subclasses concretas de Validated
link:code/23-descriptor/bulkfood/model_v5.py[role=include]
  1. Implementação do método modelo exigida pelo método abstrado Validated.validate.

  2. Se não sobrar nada após a remoção os espaços em branco antes e depois do valor, este é rejeitado.

  3. 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.

Exemplo 7. bulkfood_v5.py: LineItem usando os descritores Quantity e NonBlank
link:code/23-descriptor/bulkfood/bulkfood_v5.py[role=include]
  1. Importa o módulo model_v5, dando a ele um nome amigável.

  2. 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.

Descritores dominantes versus descritores não dominantes

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 __get__ e __set__ no Exemplo 8 chamam print_args. Assim, suas invocações são apresentadas de uma forma legível. Entender print_args e suas funções auxiliares, cls_name e display, não é importante, então não se deixe distrair por elas.

Exemplo 8. descriptorkinds.py: classes simples para estudar os comportamentos dominantes de descritores
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. Uma classe descritora dominante com __get__ e __set__.

  2. A função print_args é chamada por todos os métodos do descritor neste exemplo.

  3. Um descritor dominante sem um método __get__.

  4. Nenhum método __set__ aqui, estão este é um descritor não dominante.

  5. A classe gerenciada, usando uma instância de cada uma das classes descritoras.

  6. 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.

Descritores dominantes

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.

Exemplo 9. O comportamento de um descritor dominante
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. Cria o objeto Managed, para testes.

  2. obj.over aciona o método __get__ do descritor, passando a instância gerenciada obj como segundo argumento.

  3. Managed.over aciona o método __get__ do descritor, passando None como segundo argumento (instance).

  4. Atribuir a obj.over aciona o método __set__ do descritor, passando o valor 7 como último argumento.

  5. Ler obj.over ainda invoca o método __get__ do descritor.

  6. Contorna o descritor, definindo um valor diretamente no obj.__dict__.

  7. Verifica se aquele valor está no obj.__dict__, sob a chave over.

  8. Entretanto, mesmo com um atributo de instância chamado over, o descritor Managed.over continua interceptando tentativas de ler obj.over.

Descritor dominante sem __get__

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.

Exemplo 10. Descritor dominante sem __get__
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. Este descritor dominante não tem um método __get__, então ler obj.over_no_get obtém a instância do descritor a partir da classe.

  2. A mesma coisa acontece se obtivermos a instância do descritor diretamente da classe gerenciada.

  3. Tentar definir um valor para obj.over_no_get invoca o método __set__ do descritor.

  4. Como nosso __set__ não faz modificações, ler obj.over_no_get novamente obtém a instância do descritor na classe gerenciada.

  5. Percorrendo o __dict__ da instância para definir um atributo de instância chamado over_no_get.

  6. Agora aquele atributo de instância over_no_get oculta o descritor, mas apenas para leitura.

  7. Tentar atribuir um valor a obj.over_no_get continua passando pelo set do descritor.

  8. Mas, para leitura, aquele descritor é ocultado enquanto existir um atributo de instância de mesmo nome.

Descritor não dominante

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.

Exemplo 11. Comportamento de um descritor não dominante
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. obj.non_over aciona o método __get__ do descritor, passando obj como segundo argumento.

  2. Managed.non_over é um descritor não dominante, então não há um __set__ para interferir com essa atribuição.

  3. O obj agora tem um atributo de instância chamado non_over, que oculta o atributo do descritor de mesmo nome na classe Managed.

  4. O descritor Managed.non_over ainda está lá, e intercepta esse acesso através da classe.

  5. Se o atributo de instância non_over for excluído…​

  6. …​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.

Sobrescrevendo um descritor em uma classe

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.

Exemplo 12. Qualquer descritor pode ser sobrescrito na própria classe
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. Cria uma nova instância para testes posteriores.

  2. Sobrescreve os atributos dos descritores na classe.

  3. 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 é type, e não podemos acrescentar atributos a type. Mas, no [class_metaprog], vamos criar nossas próprias metaclasses.

Vamos ver agora como descritores são usados para implementar métodos no Python.

Métodos são descritores

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.

Exemplo 13. Um método é um descritor não dominante
link:code/23-descriptor/descriptorkinds.py[role=include]
  1. Ler de obj.spam obtém um objeto método vinculado.

  2. Mas ler de Managed.spam obtém uma função.

  3. Atribuir um valor a obj.spam oculta o atributo de classe, tornando o método spam inacessível a partir da instância obj.

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.

Exemplo 14. method_is_descriptor.py: uma classe 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.

Exemplo 15. Experimentos com um método
link:code/23-descriptor/method_is_descriptor.py[role=include]
  1. O repr de uma instância de Text se parece com uma chamada ao construtor de Text que criaria uma instância idêntica.

  2. O método reverse devolve o texto escrito de trás para frente.

  3. Um método invocado na classe funciona como uma função.

  4. Observe os tipos diferentes: uma function e um method.

  5. Text.reverse opera como uma função, mesmo ao trabalhar com objetos que não são instâncias de Text.

  6. 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.

  7. Invocar o __get__ da função com None como argumento instance obtém a própria função.

  8. A expressão word.reverse na verdade invoca Text.reverse.__get__(word), devolvendo o método vinculado.

  9. O objeto método vinculado tem um atributo __self__, contendo uma referência à instância na qual o método foi invocado.

  10. 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.

Dicas para o uso de descritores

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 um AttributeError: 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 um AttributeError 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 argumento value 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, como my_obj.the_method = 7, significa que acessos posteriores a the_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 como x.__class__.__repr__(x), então um atributo __repr__, definido em x, não tem qualquer efeito em repr(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 FrozenJSON no [ex_explore1] está a salvo de atributos de instância ocultando métodos, pois seus únicos métodos são métodos especiais e o método de classe build. Métodos de classe são seguros desde que sejam sempre acessados através da classe, como fiz com FrozenJSON.build no [ex_explore1]—mais tarde substituído por __new__ no [ex_explore2]. As classes Record e Event, apresentadas na [computed_props_sec], também estão a salvo: elas implementam apenas métodos especiais, métodos estáticos e propriedades. Propriedades são descritores dominantes, então não são ocultados por atributos de instância.

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.

Docstrings de descritores e a sobreposição de exclusão

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.

Capturas de tela do console do Python com a ajuda de descritores.
Figura 4. Capturas de tela do console do Python após os comandos help(LineItem.weight) e help(LineItem).

Resumo do capítulo

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 __set_name__ do protocolo descritor, adicionado no Python 3.6. Isso é evolução da linguagem!

Leitura complementar

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 __set_name__ não era suportado nas versões do Python anteriores a 3.6.

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.

Ponto de vista

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").


1. Raymond Hettinger, "HowTo - Guia de descritores".
2. Classes e instâncias são representadas por retângulos em diagramas de classe UML. Há diferenças visuais, mas instâncias raramente aparecem em diagramas de classe, entao desenvolvedores podem não reconhecê-las como tal.
3. O quilo de trufas brancas custa milhares de reais. Impedir a venda de trufas por $0,01 fica como exercício para a leitora com espírito de aventura. Conheço um caso real, de uma pessoa que comprou uma enciclopédia de estatísticas de 1.800 dólares por 18 dólares, devido a um erro em uma loja online(neste caso não foi na Amazon.com).
4. Mais precisamente, __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.
5. Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, p. 326. (Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos)
6. Slide #50 da palestra "Python Design Patterns" (Padrões de Projeto do Python) (EN), de Alex Martelli. Altamente recomendada.
7. Um método __delete__ também é fornecido pelo decorador property, mesmo se você não definir um método deleter (de exclusão).
8. O Python não é consistente nessas mensagens. Tentar modificar o atributo 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).
9. Entretanto, lembre-se que criar atributos de instância após o método __init__ frustra a otimização de memória através de compartilhamento de chaves, como discutido na [consequences_dict_internals].
10. Personalizar o texto de ajuda para cada instância do descritor é supreendentemente difícil. Uma solução exige criar dinamicamente uma classe invólucro (wrapper) para cada instância do descritor.