Skip to content

Latest commit

 

History

History
1182 lines (895 loc) · 89.3 KB

cap22.adoc

File metadata and controls

1182 lines (895 loc) · 89.3 KB

Atributos dinâmicos e propriedades

A importância crucial das propriedades é que sua existência torna perfeitamente seguro, e de fato aconselhável, expor atributos públicos de dados como parte da interface pública de sua classe.[1]

— Martelli, Ravenscroft & Holden
Why properties are important (Porque propriedades são importantes)

No Python, atributos de dados e métodos são conhecidos conjuntamente como atributos . Um método é um atributo invocável. Atributos dinâmicos apresentam a mesma interface que os atributos de dados—isto é, obj.attr—mas são computados sob demanda. Isso atende ao Princípio de Acesso Uniforme de Bertrand Meyer:

Todos os serviços oferecidos por um módulo deveriam estar disponíveis através de uma notação uniforme, que não revele se eles são implementados por armazenamento ou por computação.[2]

— Bertrand Meyer
Object-Oriented Software Construction (Construção de Software Orientada a Objetos)

Há muitas formas de implementar atributos dinâmicos em Python. Este capítulo trata das mais simples delas: o decorador @property e o método especial __getattr__.

Uma classe definida pelo usuário que implemente __getattr__ pode implementar uma variação dos atributos dinâmicos que chamo de atributos virtuais: atributos que não são declarados explicitamente em lugar algum no código-fonte da classe, e que não estão presentes no __dict__ das instâncias, mas que podem ser obtidos de algum outro lugar ou calculados em tempo real sempre que um usuário tenta ler um atributo inexistente tal como obj.no_such_attr.

Programar atributos dinâmicos e virtuais é o tipo de metaprogramação que autores de frameworks fazem. Entretanto, como as técnicas básicas no Python são simples, podemos usá-las nas tarefas cotidianas de processamento de dados. É por aí que iniciaremos esse capítulo.

Novidades nesse capítulo

A maioria das atualizações deste capítulo foram motivadas pela discussão relativa a @functools.cached_property (introduzido no Python 3.8), bem como pelo uso combinado de @property e @functools.cache (novo no 3.9). Isso afetou o código das classes Record e Event, que aparecem na Propriedades computadas. Também acrescentei uma refatoração para aproveitar a otimização da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves).

Para enfatizar as características mais relevantes, e ao mesmo tempo manter os exemplos legíveis, removi algum código não-essencial—fundindo a antiga classe DbRecord com Record, substituindo shelve.Shelve por um dict e suprimindo a lógica para baixar o conjunto de dados da OSCON—que os exemplos agora leem de um arquivo local, disponível no repositório de código do Python Fluente.

Processamento de dados com atributos dinâmicos

Nos próximos exemplos, vamos nos valer dos atributos dinâmicos para trabalhar com um conjunto de dados JSON publicado pela O’Reilly, para a conferência OSCON 2014. O Exemplo 1 mostra quatro registros daquele conjunto de dados.[3]

Exemplo 1. Amostra de registros do osconfeed.json; o conteúdo de alguns campos foi abreviado
link:code/22-dyn-attr-prop/oscon/osconfeed-sample.json[role=include]

O Exemplo 1 mostra 4 dos 895 registros no arquivo JSON. O conjunto dados total é um único objeto JSON, com a chave "Schedule" (Agenda), e seu valor é outro mapeamento com quatro chaves: "conferences" (conferências), "events" (eventos), "speakers" (palestrantes), e "venues" (locais). Cada uma dessas quatro últimas chaves aponta para uma lista de registros. No conjunto de dados completo, as listas de "events", "speakers" e "venues"`contêm dezenas ou centenas de registros, ao passo que `"conferences" contém apenas aquele único registro exibido no Exemplo 1. Cada registro inclui um campo "serial", que é um identificador único do registro dentro da lista.

Usei o console do Python para explorar o conjuntos de dados, como mostra o Exemplo 2.

Exemplo 2. Exploração interativa do osconfeed.json
link:code/22-dyn-attr-prop/oscon/osconfeed_explore.rst[role=include]
  1. feed é um dict contendo dicts e listas aninhados, com valores string e inteiros.

  2. Lista as quatro coleções de registros dentro de "Schedule".

  3. Exibe a contagem de registros para cada coleção.

  4. Navega pelos dicts e listas aninhados para obter o nome da última palestrante (speaker).

  5. Obtém o número de série para aquela mesma palestrante.

  6. Cada evento tem uma lista 'speakers', com o número de série de zero ou mais palestrantes.

Explorando dados JSON e similares com atributos dinâmicos

O Exemplo 2 é bastanre simples, mas a sintaxe feed['Schedule']['events'][40]['name'] é desajeitada. Em JavaScript, é possível obter o mesmo valor escrevendo feed.Schedule.events[40].name. É fácil de implementar uma classe parecida com um dict para fazer o mesmo em Python—​há inúmeras implementações na web.[4] Escrevi FrozenJSON, que é mais simples que a maioria das receitas, pois suporta apenas leitura: ela serve apenas para explorar os dados. FrozenJSON é também recursivo, lidando automaticamente com mapeamentos e listas aninhados.

O Exemplo 3 é uma demonstração da FrozenJSON, e o código-fonte aparece no Exemplo 4.

Exemplo 3. FrozenJSON, do Exemplo 4, permite ler atributos como name, e invocar métodos como .keys() e .items()
link:code/22-dyn-attr-prop/oscon/explore0.py[role=include]
  1. Cria uma instância de FrozenJSON a partir de raw_feed, feito de dicts e listas aninhados.

  2. FrozenJSON permite navegar dicts aninhados usando a notação de atributos; aqui exibimos o tamanho da lista de palestrantes.

  3. Métodos dos dicts subjacentes também podem ser acessados; por exemplo,.keys(), para recuperar os nomes das coleções de registros.

  4. Usando items(), podemos buscar os nomes das coleções de registros e seus conteúdos, para exibir o len() de cada um deles.

  5. Uma list, tal como feed.Schedule.speakers, permanece uma lista, mas os itens dentro dela, se forem mapeamentos, são convertidos em um FrozenJSON.

  6. O item 40 na lista events era um objeto JSON; agora ele é uma instância de FrozenJSON.

  7. Registros de eventos tem uma lista de speakers com os números de séries de palestrantes.

  8. Tentar ler um atributo inexistente gera uma exceção KeyError, em vez da AttributeError usual.

A pedra angular da classe FrozenJSON é o metodo __getattr__, que já usamos no exemplo Vector da [vector_dynamic_attrs_sec], para recuperar componentes de Vector por letra: v.x, v.y, v.z, etc. É essencial lembrar que o método especial __getattr__ só é invocado pelo interpretador quando o processo habitual falha em recuperar um atributo (isto é, quando o atributo nomeado não é encontrado na instância, nem na classe ou em suas superclasses).

A última linha do Exemplo 3 expõe um pequeno problema em meu código: tentar ler um atributo ausente deveria produzir uma exceção AttributeError, e não a KeyError gerada. Quando implementei o tratamento de erro para fazer isso, o método __getattr__ se tornou duas vezes mais longo, distraindo o leitor da lógica mais importante que eu queria apresentar. Dado que os usuários saberiam que uma FrozenJSON é criada a partir de mapeamentos e listas, acho que KeyError não é tão confuso assim.

Exemplo 4. explore0.py: transforma um conjunto de dados JSON em um FrozenJSON contendo objetos FrozenJSON aninhados, listas e tipos simples
link:code/22-dyn-attr-prop/oscon/explore0.py[role=include]
  1. Cria um dict a partir do argumento mapping. Isso garante que teremos um mapeamento ou algo que poderá ser convertido para isso. O prefixo de duplo sublinhado em __data o torna um atributo privado.

  2. __getattr__ é invocado apenas quando não existe um atributo com aquele name.

  3. Se name corresponde a um atributo da instância de dict __data, devolve aquele atributo. É assim que chamadas como feed.keys() são tratadas: o método keys é um atributo do dict __data.

  4. Caso contrário, obtém o item com a chave name de self.__data, e devolve o resultado da chamada FrozenJSON.build() com aquele argumento.[5]

  5. Implementar __dir__ suporta a função embutida dir(), que por sua vez suporta o preenchimento automático (auto-complete) no console padrão do Python, bem como no IPython, no Jupyter Notebook, etc. Esse código simples vai permitir preenchimento automático recursivo baseado nas chaves em self.__data, porque __getattr__ cria instâncias de FrozenJSON em tempo real—um recurso útil para a exploração interativa dos dados.

  6. Este é um construtor alternativo, um uso comum para o decorador

  7. Se obj é um mapeamento, cria um FrozenJSON com ele. Esse é um exmeplo de goose typing—veja a [goose_typing_sec] caso precise de uma revisão desse tópico.

  8. Se for uma MutableSequence, tem que ser uma lista[6], então criamos uma list, passando recursivamente cada item em obj para .build().

  9. Se não for um dict ou uma list, devolve o item com está.

Uma instância de FrozenJSON contém um atributo de instância privado __data, armazenado sob o nome _FrozenJSON__data, como explicado na [private_protected_sec]. Tentativas de recuperar atributos por outros nomes vão disparar __getattr__. Esse método irá primeiro olhar se o dict self.__data contém um atributo (não uma chave!) com aquele nome; isso permite que instâncias de FrozenJSON tratem métodos de dict tal como items, delegando para self.__data.items(). Se self.__data não contiver uma atributo como o name dado, __getattr__ usa name como chave para recuperar um item de self.__data, e passa aquele item para FrozenJSON.build. Isso permite navegar por estruturas aninhadas nos dados JSON, já que cada mapeamento aninhado é convertido para outra instância de FrozenJSON pelo método de classe build.

Observe que FrozenJSON não transforma ou armazena o conjunto de dados original. Conforme navegamos pelos dados, __getattr__ cria continuamente instâncias de FrozenJSON. Isso é aceitável para um conjunto de dados deste tamanho, e para um script que só será usado para explorar ou converter os dados.

Qualquer script que gera ou emula nomes de atributos dinâmicos a partir de fontes arbitrárias precisa lidar com uma questão: as chaves nos dados originais podem não ser nomes adequados de atributos. A próxima seção fala disso.

O problema do nome de atributo inválido

O código de FrozenJSON não aceita com nomes de atributos que sejam palavras reservadas do Python. Por exemplo, se você criar um objeto como esse

>>> student = FrozenJSON({'name': 'Jim Bo', 'class': 1982})

não será possível ler student.class, porque class é uma palavra reservada no Python:

>>> student.class
  File "<stdin>", line 1
    student.class
         ^
SyntaxError: invalid syntax

Claro, sempre é possível fazer assim:

>>> getattr(student, 'class')
1982

Mas a ideia de FrozenJSON é oferecer acesso conveniente aos dados, então uma solução melhor é verificar se uma chave no mapamento passado para FrozenJSON.__init__ é uma palavra reservada e, em caso positivo, anexar um _ a ela, de forma que o atributo possa ser acessado assim:

>>> student.class_
1982

Isso pode ser feito substituindo o __init__ de uma linha do Exemplo 4 pela versão no Exemplo 5.

Exemplo 5. explore1.py: anexa um _ a nomes de atributo que sejam palavraas reservadas do Python
link:code/22-dyn-attr-prop/oscon/explore1.py[role=include]
  1. A função keyword.iskeyword(…) é exatamente o que precisamos; para usá-la, o módulo keyword precisa ser importado; isso não aparece nesse trecho.

Um problema similar pode surgir se uma chave em um registro JSON não for um identificador válido em Python:

>>> x = FrozenJSON({'2be':'or not'})
>>> x.2be
  File "<stdin>", line 1
    x.2be
      ^
SyntaxError: invalid syntax

Essas chaves problemáticas são fáceis de detectar no Python 3, porque a classe str oferece o método s.isidentifier(), que informa se s é um identificador Python válido, de acordo com a gramática da linguagem. Mas transformar uma chave que não seja um identificador válido em um nome de atributo válido não é trivial. Uma solução seria implementar __getitem__ para permitir acesso a atributos usando uma notação como x['2be']. Em nome da simplicidade, não vou me preocupar com esse problema.

Após essa pequena conversa sobre os nomes de atributos dinâmicos, vamos examinar outra característica essencial de FrozenJSON: a lógica do método de classe build. Frozen.JSON.build é usado por __getattr__ para devolver um tipo diferente de objeto, dependendo do valor do atributo que está sendo acessado: estruturas aninhadas são convertidas para instâncias de FrozenJSON ou listas de instâncias de FrozenJSON.

Em vez de usar um método de classe, a mesma lógica poderia ser implementada com o método especial __new__, como veremos a seguir.

Criação flexível de objetos com __new__

Muitas vezes nos referimos ao __init__ como o método construtor, mas isso é porque adotamos o jargão de outras linguagens. No Python, __init__ recebe self como primeiro argumentos, portanto o objeto já existe quando __init__ é invocado pelo interpretador. Além disso, __init__ não pode devolver nada. Então, na verdade, esse método é um inicializador, não um construtor.

Quando uma classe é chamada para criar uma instância, o método especial chamado pelo Python naquela classe para construir a instância é __new__. É um método de classe, mas recebe tratamento especial, então o decorador @classmethod não é aplicado a ele. O Python recebe a instância devolvida por __new__, e daí a passa como o primeiro argumento (self) para __init__. Raramente precisamos escrever um __new__, pois a implementação herdada de object é suficiente na vasta maioria dos casos.

Se necessário, o método __new__ pode também devolver uma instância de uma classe diferente. Quando isso acontece, o interpretador não invoca __init__. Em outras palavras, a lógica do Python para criar um objeto é similar a esse pseudo-código:

link:code/22-dyn-attr-prop/pseudo_construction.py[role=include]

O Exemplo 6 mostra uma variante de FrozenJSON onde a lógica da antiga classe build foi transferida para o método __new__.

Exemplo 6. explore2.py: usando __new__ em vez de build para criar novos objetos, que podem ou não ser instâncias de FrozenJSON
link:code/22-dyn-attr-prop/oscon/explore2.py[role=include]
  1. Como se trata de um método de classe, o primeiro argumento recebido por __new__ é a própria classe, e os argumentos restantes são os mesmos recebido por __init__, exceto por self.

  2. O comportamento default é delegar para o __new__ de uma superclasse. Nesse caso, estamos invocando o __new__ da classe base object, passando FrozenJSON como único argumento.

  3. As linhas restantes de __new__ são exatamente as do antigo método build.

  4. Era daqui que FrozenJSON.build era chamado antes; agora chamamos apenas a classe FrozenJSON, e o Python trata essa chamada invocando FrozenJSON.__new__.

O método __new__ recebe uma classe como primeiro argumento porque, normalmente, o objeto criado será uma instância daquela classe. Então, em FrozenJSON.__new__, quando a expressão super().__new__(cls) efetivamente chama object.__new__(FrozenJSON), a instância criada pela classe object é, na verdade, uma instância de FrozenJSON. O atributo __class__ da nova instância vai manter uma referência para FrozenJSON, apesar da construção concreta ser realizada por object.__new__, implementado em C, nas entranhas do interpretador.

O conjunto de dados da OSCON está estruturado de uma forma pouco amigável à exploração interativa. Por exemplo, o evento no índice 40, chamado 'There Will Be Bugs' (Haverá Bugs) tem dois palestrantes, 3471 e 5199. Encontrar os nomes dos palestrantes é confuso, pois esses são números de série e a lista Schedule.speakers não está indexada por eles. Para obter cada palestrante, precisamos iterar sobre a lista até encontrar um registro com o número de série correspondente. Nossa próxima tarefa é reestruturar os dados para preparar a recuperação automática de registros relacionados.

Propriedades computadas

Vimos inicialmente o decorador @property no [pythonic_objects], na [hashable_vector2d]. No [ex_vector2d_v3], usei duas propriedades no Vector2d apenas para tornar os atributos x e y apenas para leitura. Aqui vamos ver propriedades que calculam valores, levando a uma discussão sobre como armazenar tais valores.

Os registros na lista 'events' dos dados JSON da OSCON contêm números de série inteiros apontando para registros nas listas 'speakers' e 'venues'. Por exemplo, esse é o registro de uma palestra (com a descrição parcial terminando em reticências):

link:code/22-dyn-attr-prop/oscon/osconfeed-talk.json[role=include]

Vamos implementar uma classe Event com propriedades venue e speakers, para devolver automaticamente os dados relacionados—​em outras palavras, "derreferenciar" o número de série. Dada uma instância de Event, o Exemplo 7 mostra o comportamento desejado.

Exemplo 7. Ler venue e speakers devolve objetos Record
link:code/22-dyn-attr-prop/oscon/schedule_v4.py[role=include]
  1. Dada uma instância de Event,…​

  2. …​ler event.venue devolve um objeto Record em vez de um número de série.

  3. Agora é fácil obter o nome do venue.

  4. A propriedade event.speakers devolve uma lista de instâncias de Record.

Como sempre, vamos criar o código passo a passo, começando com a classe Record e uma função para ler dados JSON e devolver um dict com instâncias de Record.

Passo 1: criação de atributos baseados em dados

O Exemplo 8 mostra o doctest para orientar esse primeiro passo.

Exemplo 8. Testando schedule_v1.py (do Exemplo 9)
link:code/22-dyn-attr-prop/oscon/schedule_v1.py[role=include]
  1. load um dict com os dados JSON.

  2. As chaves em records são strings criadas a partir do tipo de registro e do número de série.

  3. speaker é uma instância da classe Record, definida no Exemplo 9.

  4. Campos do JSON original podem ser acessados como atributos de instância de Record.

O código de schedule_v1.py está no Exemplo 9.

Exemplo 9. schedule_v1.py: reorganizando os dados de agendamento da OSCON
link:code/22-dyn-attr-prop/oscon/schedule_v1.py[role=include]
  1. Isso é um atalho comum para construir uma instância com atributos criados a partir de argumentos nomeados (a explicação detalhada está abaixo) .

  2. Usa o campo serial para criar a representação personalizada de Record exibida no Exemplo 8.

  3. load vai por fim devolver um dict de instâncias de Record.

  4. Analisa o JSON, devolvendo objetos Python nativos: listas, dicts, strings, números, etc.

  5. Itera sobre as quatro listas principais, chamadas 'conferences', 'events', 'speakers', e 'venues'.

  6. record_type é o nome da lista sem o último caractere, então speakers se torna speaker. No Python ≥ 3.9, podemos fazer isso de forma mais explícita com collection.removesuffix('s')—veja a PEP 616—String methods to remove prefixes and suffixes (Métodos de string para remover prefixos e sufixos_).

  7. Cria a key no formato 'speaker.3471'.

  8. Cria uma instância de Record e a armazena em records com a chave key.

O método Record.__init__ ilustra um antigo truque do Python. Lembre-se que o __dict__ de um objeto é onde são mantidos seus atributos—​a menos que __slots__ seja declarado na classe, como vimos na [slots_section]. Daí, atualizar o __dict__ de uma instância é uma maneira fácil de criar um punhado de atributos naquela instância.[7]

Note

Dependendo da aplicação, a classe Record pode ter que lidar com chaves que não sejam nomes de atributo válidos, como vimos na O problema do nome de atributo inválido. Tratar essa questão nos distrairia da ideia principal desse exemplo, e não é um problema no conjunto de dados que estamos usando.

A definição de Record no Exemplo 9 é tão simples que você pode estar se perguntando porque não a usei antes, em vez do mais complicado FrozenJSON. São duas razões. Primeiro, FrozenJSON funciona convertendo recursivamente os mapeamentos aninhados e listas; Record não precisa fazer isso, pois nosso conjunto de dados convertido não contém mapeamentos aninhados ou listas. Os registros contêm apenas strings, inteiros, listas de strings e listas de inteiros. A segunda razão: FrozenJSON oferece acesso aos atributos no dict embutido __data—que usamos para invocar métodos como .keys()—e agora também não precisamos mais dessa funcionalidade.

Note

A biblioteca padrão do Python oferece classes similares a Record, onde cada instância tem um conjunto arbitrário de atributos criados a partir de argumentos nomeados passados a __init__: types.SimpleNamespace, argparse.Namespace (EN), and multiprocessing.managers.Namespace (EN). Escrevi a classe Record, mais simples, para destacar a ideia essencial: __init__ atualizando o __dict__ da instância.

Após reorganizar o conjunto de dados de agendamento, podemos aprimorar a classe Record para obter automaticamente registros de venue e speaker referenciados em um registro event. Vamos utilizar propriedades para fazer exatamente isso nos próximos exemplos.

Passo 2: Propriedades para recuperar um registro relacionado

O objetivo da próxima versão é: dado um registro event, ler sua propriedade venue vai devolver um Record. Isso é similar ao que o ORM (Object Relational Mapping, Mapeamento Relacional de Objetos) do Django faz quando acessamos um campo ForeignKey: em vez da chave, recebemos o modelo de objeto relacionado.

Vamos começar pela propriedade venue. Veja a interação parcial no Exemplo 10.

Exemplo 10. Extratos dos doctests de schedule_v2.py
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
  1. O método estático Record.fetch obtém um Record ou um Event do conjunto de dados.

  2. Observe que event é uma instância da classe Event.

  3. Acessar event.venue devolve uma instância de Record.

  4. Agora é fácil encontrar o nome de um event.venue.

  5. A instância de Event também tem um atributo venue_serial, vindo dos dados JSON.

Event é uma subclasse de Record, acrescentando um venue para obter os registros relacionados, e um método __repr__ especializado.

O código dessa seção está no módulo schedule_v2.py, no repositório de código do Python Fluente. O exemplo tem aproximadamente 50 linhas, então vou apresentá-lo em partes, começando pela classe Record aperfeiçoada.

Exemplo 11. schedule_v2.py: a classe Record com um novo método fetch
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
  1. inspect será usado em load, lista do no Exemplo 13.

  2. No final, o atributo de classe privado __index manterá a referência ao dict devolvido por load.

  3. fetch é um staticmethod, para deixar explícito que seu efeito não é influenciado pela classe ou pela instância de onde ele é invocado.

  4. Preenche o Record.__index, se necessário.

  5. E o utiliza para obter um registro com uma dada key.

Tip

Esse é um exemplo onde o uso de staticmethod faz sentido. O método fetch sempre age sobre o atributo de classe Record.__index, mesmo quando invocado desde uma subclasse, como Event.fetch()—que exploraremos a seguir. Seria equivocado programá-lo como um método de classe, pois o primeiro argumento, cls, nunca é usado.

Agora podemos usar a propriedade na classe Event, listada no Exemplo 12.

Exemplo 12. schedule_v2.py: a classe Event
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
  1. Event estende Record.

  2. Se a instância tem um atributo name, esse atributo será usado para produzir uma representação personalizada. Caso contrário, delega para o __repr__ de Record.

  3. A propriedade venue cria uma key a partir do atributo venue_serial, e a passa para o método de classe fetch, herdado de Record (a razão para usar self.__class__ logo ficará clara).

A segunda linha do método venue no Exemplo 12 devolve self​.__class__.fetch(key). Por que não podemos simplesmente invocar self.fetch(key)? A forma simples funciona com esse conjunto específico de dados da OSCON porque não há registro de evento com uma chave 'fetch'. Mas, se um registro de evento possuísse uma chave chamada 'fetch', então dentro daquela instância específica de Event, a referência self.fetch apontaria para o valor daquele campo, em vez do método de classe fetch que Event herda de Record. Esse é um bug sutil, e poderia facilmente escapar aos testes, pois depende do conjunto de dados.

Warning

Ao criar nomes de atributos de instância a partir de dados, sempre existe o risco de bugs causados pelo ocultamento de atributos de classe—tais como métodos—ou pela perda de dados por sobrescrita acidental de atributos de instância existentes. Esses problemas talvez expliquem, mais que qualquer outra coisa, porque os dicts do Python não são como objetos Javascript.

Se a classe Record se comportasse mais como um mapeamento, implementando um __getitem__ dinâmico em vez de um __getattr__ dinâmico, não haveria risco de bugs por ocultamento ou sobrescrita. Um mapeamento personalizado seria provavelmente a forma pythônica de implementar Record. Mas se eu tivesse seguido por aquele caminho, não estaríamos estudando os truques e as armadilhas da programação dinâmica de atributos.

A parte final deste exemplo é a função load revisada, no Exemplo 13.

Exemplo 13. schedule_v2.py: a função load
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
  1. Até aqui, nenhuma mudança em relação ao load em schedule_v1.py (do Exemplo 9).

  2. Muda a primeira letra de record_type para maiúscula, para obter um possível nome de classe; por exemplo, 'event' se torna 'Event'.

  3. Obtém um objeto com aquele nome do escopo global do módulo; se aquele objeto não existir, obtém a classe Record.

  4. Se o objeto recém-obtido é uma classe, e é uma subclasse de Record…​

  5. …​vincula o nome factory a ele. Isso significa que factory pode ser qualquer subclasse de Record, dependendo do record_type.

  6. Caso contrário, vincula o nome factory a Record.

  7. O loop for, que cria a key e armazena os registros, é o mesmo de antes, exceto que…​

  8. …​o objeto armazenado em records é construído por factory, e pode ser Record ou uma subclasse, como Event, selecionada de acordo com o record_type.

Observe que o único record_type que tem uma classe personalizada é Event, mas se classes chamadas Speaker ou Venue existirem, load vai automaticamente usar aquelas classes ao criar e armazenar registros, em vez da classe default Record.

Vamos agora aplicar a mesma ideia à nova propriedade speakers, na classe Events.

Passo 3: Uma propriedade sobrepondo um atributo existente

O nome da propriedade venue no Exemplo 12 não corresponde a um nome de campo nos registros da coleção "events". Seus dados vem de um nome de campo venue_serial. Por outro lado, cada registro na coleção events tem um campo speakers, contendo uma lista de números de série. Queremos expor essa informação na forma de uma propriedade speakers em instâncias de Event, que devolve um lista de instâncias de Record. Essa colisão de nomes exige uma atenção especial, como revela o Exemplo 14.

Exemplo 14. schedule_v3.py: a propriedade speakers
link:code/22-dyn-attr-prop/oscon/schedule_v3.py[role=include]
  1. Os dados que precisamos estão em um atributo speakers, mas precisamos obtê-los diretamente do __dict__ da instância, para evitar uma chamada recursiva à propriedade speakers.

  2. Devolve uma lista de todos os registros com chaves correspondendo aos números em spkr_serials.

Dentro do método speakers, tentar ler self.speakers irá invocar a própria propriedade, gerando rapidamente um RecursionError. Entretanto, se lemos os mesmos dados via self.__dict__['speakers'], o algoritmo normal do Python para busca e recuperação de atributos é ignorado, a propriedade não é chamada e a recursão é evitada. Por essa razão, ler ou escrever dados diretamente no __dict__ de um objeto é um truque comum em metaprogramação no Python.

Warning

O interpretador avalia obj.my_attr olhando primeiro a classe de obj. Se a classe possuir uma propriedade de nome my_attr, aquela propriedade oculta um atributo de instância com o mesmo nome. Isso será demonstrado por exemplos na Propriedades sobrepõe atributos de instância, e o [attribute_descriptors] vai revelar que uma propriedade é implementada como um descritor—uma abstração mais geral e poderosa.

Quando programava a compreensão de lista no Exemplo 14, meu cérebro réptil de programador pensou: "Isso talvez seja custoso". Na verdade não é, porque os eventos no conjuntos de dados da OSCON contêm poucos palestrantes, então programar algo mais complexo seria uma otimização prematura. Entretanto, criar um cache de uma propriedade é uma necessidade comum—e há ressalvas. Vamos ver então, nos próximos exemplos, como fazer isso.

Passo 4: Um cache de propriedades sob medida

Fazer caching de propriedades é uma necessidade comum, pois há a expectativa de que uma expressão como event.venue deveria ser pouco dispendiosa.[8] Alguma forma de caching poderia se tornar necessário caso o método Record.fetch, subjacente às propriedades de Event, precise consultar um banco de dados ou uma API web.

Na primeira edição de Python Fluente, programei a lógica personalizada de caching para o método speakers, como mostra o Exemplo 15.

Exemplo 15. A lógica de caching personalizada usando hasattr desabilita a otimização de compartilhamento de chaves
link:code/22-dyn-attr-prop/oscon/schedule_v4_hasattr.py[role=include]
  1. Se a instância não tem um atributo chamado __speaker_objs, obtém os objetos speaker e os armazena ali..

  2. Devolve self.__speaker_objs.

O caching caseiro no Exemplo 15 é bastante direto, mas criar atributos após a inicialização da instância frustra a otimização da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves), como explicado na [consequences_dict_internals]. Dependendo do tamanho da massa de dados, a diferença de uso de memória pode ser importante.

Uma solução manual similar, que funciona bem com a otimização de compartilhamento de chaves, exige escrever um __init__ para a classe Event, para criar o necessário __speaker_objs inicializado para None, e então usá-lo no método speakers. Veja o Exemplo 16.

Exemplo 16. Armazenamento definido em __init__ para manter a otimização de compartilhamento de chaves
link:code/22-dyn-attr-prop/oscon/schedule_v4.py[role=include]
# 15 lines omitted...
link:code/22-dyn-attr-prop/oscon/schedule_v4.py[role=include]

O Exemplo 15 e o Exemplo 16 ilustram técnicas simples de caching bastante comuns em bases de código Python legadas. Entretanto, em programas com múltiplas threads, caches manuais como aqueles introduzem condições de concorrência (ou de corrida) que podem levar à corrupção de dados. Se duas threads estão lendo uma propriedade que não foi armazenada no cache anteriormente, a primeira thread precisará computar os dados para o atributo de cache (_speaker_objs nos exemplos) e a segunda thread corre o risco de ler um valor incompleto do _cache.

Felizmente, o Python 3.8 introduziu o decorador @functools.cached_property, que é seguro para uso com threads. Infelizmente, ele vem com algumas ressalvas, discutidas a seguir.

Passo 5: Caching de propriedades com functools

O módulo functools oferece três decoradores para caching. Vimos @cache e @lru_cache na [memoization_sec] (no [closures_and_decorators]). O Python 3.8 introduziu @cached_property.

O decorador functools.cached_property faz cache do resultado de um método em uma variável de instância com o mesmo nome.

Por exemplo, no Exemplo 17, o valor computado pelo método venue é armazenado em um atributo venue, em self. Após isso, quando código cliente tenta ler venue, o recém-criado atributo de instância venue é usado, em vez do método.

Exemplo 17. Uso simples de uma @cached_property
link:code/22-dyn-attr-prop/oscon/schedule_v5.py[role=include]

Na Passo 3: Uma propriedade sobrepondo um atributo existente, vimos que uma propriedade oculta um atributo de instância de mesmo nome. Se isso é verdade, como @cached_property pode funcionar? Se a propriedade se sobrepõe ao atributo de instância, o atributo venue será ignorado e o método venue será sempre chamado, computando a key e rodando fetch todas as vezes!

A resposta é um tanto triste: cached_property é um nome enganador. O decorador @cached_property não cria uma propriedade completa, ele cria um descritor não dominante. Um descritor é um objeto que gerencia o acesso a um atributo em outra classe. Vamos mergulhar nos descritores no [attribute_descriptors]. O decorador property é uma API de alto nível para criar um descritor dominante. O [attribute_descriptors] inclui um explicação completa sobre descritores dominantes e não dominantes.

Por hora, vamos deixar de lado a implementação subjacente e nos concentrar nas diferenças entre cached_property e property do ponto de vista de um usuário. Raymond Hettinger os explica muito bem na Documentação do Python:

A mecânica de cached_property() é um tanto diferente da de property(). Uma propriedade regular bloqueia a escrita em atributos, a menos que um setter seja definido. Uma cached_property, por outro lado, permite a escrita.

O decorador cached_property só funciona para consultas e apenas quando um atributo de mesmo nome não existe. Quando funciona, cached_property escreve no atributo de mesmo nome. Leituras e escritas subsequentes do/no atributo tem precedência sobre o método decorado com cached_property e ele funciona como um atributo normal.

O valor em cache pode ser excluído apagando-se o atributo. Isso permite que o método cached_property rode novamente.[9]

Voltando à nossa classe Event: o comportamento específico de @cached_property o torna inadequado para decorar speakers, porque aquele método depende de um atributo existente também chamado speakers, contendo os números de série dos palestrantes do evento.

Warning

@cached_property tem algumas importantes limitações:

  • Ele não pode ser usado como um substituto direto de @property se o método decorado já depender de um atributo de instância de mesmo nome.

  • Ele não pode ser usado em uma classe que defina __slots__.

  • Ele impede a otimização de compartilhamento de chaves do __dict__ da instância, pois cria um atributo de instância após o __init__.

Apesar dessas limitações, @cached_property supre uma necessidade comum de uma maneira simples, e é seguro para usar com threads. Seu código Python é um exemplo do uso de uma trava recursiva (reentrant lock).

A documentação de @cached_property recomenda uma solução altenativa que podemos usar com speakers: Empilhar decoradores @property e @cache, como exibido no Exemplo 18.

Exemplo 18. Stacking @property sobre @cache
link:code/22-dyn-attr-prop/oscon/schedule_v5.py[role=include]
  1. A ordem é importante: @property vai acima…​

  2. …​de @cache.

Lembre-se do significado dessa sintaxe, comentada em [stacked_decorators_tip]. A três primeiras linhas do Exemplo 18 são similares a :

speakers = property(cache(speakers))

O @cache é aplicado a speakers, devolvendo uma nova função. Essa função é então decorada por @property, que a substitui por uma propriedade recém-criada.

Isso encerra nossa discussão de propriedades somente para leitura e decoradores de caching, explorando o conjunto de dados da OSCON.

Na próxima seção iniciamos uma nova série de exemplos, criando propriedades de leitura e escrita.

Usando uma propriedade para validação de atributos

Além de computar valores de atributos, as propriedades também são usadas para impor regras de negócio, transformando um atributo público em um atributo protegido por um getter e um setter, sem afetar o código cliente. Vamos explorar um exemplo estendido.

LineItem Versão #1: Um classe para um item em um pedido

Imagine uma aplicação para uma loja que vende comida orgânica a granel, onde os fregueses podem encomendar nozes, frutas secas e cereais por peso. Nesse sistema, cada pedido mantém uma sequência de produtos, e cada produto pode ser representado por uma instância de uma classe, como no Exemplo 19.

Exemplo 19. bulkfood_v1.py: a classe LineItem mais simples
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v1.py[role=include]

Esse código é simples e agradável. Talvez simples demais. Exemplo 20 mostra um problema.

Exemplo 20. Um peso negativo resulta em um subtotal negativo
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v1.py[role=include]

Apesar desse ser um exemplo inventado, não é tão fantasioso quanto se poderia imaginar. Aqui está uma história do início da Amazon.com:

Descobrimos que os clientes podiam encomendar uma quantidade negativa de livros! E nós creditaríamos seus cartões de crédito com o preço e, suponho, esperaríamos que eles nos enviassem os livros.[10]

— Jeff Bezos
fundador e CEO da Amazon.com

Como consertar isso? Poderíamos mudar a interface de LineItem para usar um getter e um setter para o atributo weight. Esse seria o caminho do Java, e não está errado. Por outro lado, é natural poder determinar o weight (peso) de um item apenas atribuindo um valor a ele; e talvez o sistema esteja em produção, com outras partes já acessando item.weight diretamente. Nesse caso, o caminho do Python seria substituir o atributo de dados por uma propriedade.

LineItem versão #2: Uma propriedade de validação

Implementar uma propriedade nos permitirá usar um getter e um setter, mas a interface de LineItem não mudará (isto é, definir o weight de um LineItem ainda será escrito no formato raisins.weight = 12).

O Exemplo 21 lista o código para uma propriedade de leitura e escrita de weight.

Exemplo 21. bulkfood_v2.py: um LineItem com uma propriedade weight
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2.py[role=include]
  1. Aqui o setter da propriedade já está em uso, assegurando que nenhuma instância com peso negativo possa ser criada.

  2. @property decora o método getter.

  3. Todos os métodos que implementam a propriedade compartilham o mesmo nome, do atributo público: weight.

  4. O valor efetivo é armazenado em um atributo privado __weight.

  5. O getter decorado tem um atributo .setter, que também é um decorador; isso conecta o getter e o setter.

  6. Se o valor for maior que zero, definimos o __weight privado.

  7. Caso contrário, uma ValueError é gerada.

Observe como agora não é possível criar uma LineItem com peso inválido:

>>> walnuts = LineItem('walnuts', 0, 10.00)
Traceback (most recent call last):
    ...
ValueError: value must be > 0

Agora protegemos weight impedindo que usuários forneçam valores negativos. Apesar de compradores normalmente não poderem definir o preço de um produto, um erro administrativo ou um bug poderiam criar um LineItem com um price negativo. Para evitar isso, poderíamos também transformar price em uma propriedade, mas isso levaria a alguma repetição no nosso código.

Lembre-se da citação de Paul Graham no [iterables2generators]: "Quando vejo padrões em meus programas, considero isso um mau sinal." A cura para a repetição é a abstração. Há duas maneiras de abstrair definições de propriedades: usar uma fábrica de propriedades ou uma classe descritora. A abordagem via classe descritora é mais flexível, e dedicaremos o [attribute_descriptors] a uma discussão completa desse recurso. Na verdade, propriedades são, elas mesmas, implementadas como classes descritoras. Mas aqui vamos seguir com nossa exploração das propriedades, implementando uma fábrica de propriedades em forma de função.

Mas antes de podermos implementar uma fábrica de propriedades, precisamos entender melhor as propriedades em si.

Considerando as propriedades de forma adequada

Apesar de ser frequentemente usada como um decorador, property é na verdade uma classe embutida. No Python, funções e classes são muitas vezes intercambiáveis, pois ambas são invocáveis e não há um operador new para instanciação de objeto, então invocar um construtor não é diferente de invocar uma função fábrica. E ambas podem ser usadas como decoradores, desde que elas devolvam um novo invocável, que seja um substituto adequado do invocável decorado.

Essa é a assinatura completa do construtor de property:

property(fget=None, fset=None, fdel=None, doc=None)

Todos os argumentos são opcionais, e se uma função não for fornecida para algum deles, a operação correspondente não será permitida pelo objeto propriedade resultante.

O tipo property foi introduzido no Python 2.2, mas a sintaxe @ do decorador só surgiu no Python 2.4. Então, por alguns anos, propriedades eram definidas passando as funções de acesso nos dois primeiros argumentos.

A sintaxe "clássica" para definir propriedades sem decoradores é ilustrada pelo Exemplo 22.

Exemplo 22. bulkfood_v2b.py: igual ao Exemplo 21, mas sem usar decoradores
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2b.py[role=include]
  1. Um getter simples.

  2. Um setter simples.

  3. Cria a property e a vincula a um atributo de classe simples.

Em algumas situações, a forma clássica é melhor que a sintaxe do decorador; o código da fábrica de propriedade, que discutiremos em breve, é um exemplo. Por outro lado, no corpo de uma classe com muitos métodos, os decoradores tornam explícito quais são os getters e os setters, sem depender da convenção do uso dos prefixos get e set em seus nomes.

A presença de uma propriedade em uma classe afeta como os atributos nas instâncias daquela classe podem ser encontrados, de uma forma que à primeira vista pode ser surpreendente. A próxima seção explica isso.

Propriedades sobrepõe atributos de instância

Propriedades são sempre atributos de classe, mas elas na verdade gerenciam o acesso a atributos nas instâncias da classe.

Na [overriding_class_attributes], vimos que quando uma instância e sua classe tem um atributo de dados com o mesmo nome, o atributo de instância sobrepõe, ou oculta, o atributo da classe—ao menos quando lidos através daquela instância. O Exemplo 23 ilustra esse ponto.

Exemplo 23. Atributo de instância oculta o atributo de classe data
>>> class Class:  # (1)
...     data = 'the class data attr'
...     @property
...     def prop(self):
...         return 'the prop value'
...
>>> obj = Class()
>>> vars(obj)  # (2)
{}
>>> obj.data  # (3)
'the class data attr'
>>> obj.data = 'bar' # (4)
>>> vars(obj)  # (5)
{'data': 'bar'}
>>> obj.data  # (6)
'bar'
>>> Class.data  # (7)
'the class data attr'
  1. Define Class com dois atributos de classe: o atributo data e a propriedade prop.

  2. vars devolve o __dict__ de obj, mostrando que ele não tem atributos de instância.

  3. Ler de obj.data obtém o valor de Class.data.

  4. Escrever em obj.data cria um atributo de instância.

  5. Inspeciona a instância, para ver o atributo de instância.

  6. Ler agora de obj.data obtém o valor do atributo da instância. Quanto lido a partir da instância obj, o data da instância oculta o data da classe.

  7. O atributo Class.data está intacto.

Agora vamos tentar sobrepor o atributo prop na instância obj. Continuando a sessão de console anterior, temos o Exemplo 24.

Exemplo 24. Um atributo de instância não oculta uma propriedade da classe (continuando do Exemplo 23)
>>> Class.prop  # (1)
<property object at 0x1072b7408>
>>> obj.prop  # (2)
'the prop value'
>>> obj.prop = 'foo'  # (3)
Traceback (most recent call last):
  ...
AttributeError: can't set attribute
>>> obj.__dict__['prop'] = 'foo'  # (4)
>>> vars(obj)  # (5)
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop  # (6)
'the prop value'
>>> Class.prop = 'baz'  # (7)
>>> obj.prop  # (8)
'foo'
  1. Ler prop diretamente de Class obtém o próprio objeto propriedade, sem executar seu método getter.

  2. Ler obj.prop executa o getter da propriedade.

  3. Tentar definir um atributo prop na instância falha.

  4. Inserir 'prop' diretamente em obj.__dict__ funciona.

  5. Podemos ver que agora obj tem dois atributos de instância: data e prop.

  6. Entretanto, ler obj.prop ainda executa o getter da propriedade. A propriedade não é ocultada pelo atributo de instância.

  7. Sobrescrever Class.prop destrói o objeto propriedade.

  8. Agora obj.prop obtém o atributo de instância. Class.prop não é mais uma propriedade, então ela não mais sobrepõe obj.prop.

Como uma demonstração final, vamos adicionar uma propriedade a Class, e vê-la sobrepor um atributo de instância. O Exemplo 25 retoma a sessão onde Exemplo 24 parou.

Exemplo 25. Uma nova propriedade de classe oculta o atributo de instância existente (continuando do Exemplo 24)
>>> obj.data  # (1)
'bar'
>>> Class.data  # (2)
'the class data attr'
>>> Class.data = property(lambda self: 'the "data" prop value')  # (3)
>>> obj.data  # (4)
'the "data" prop value'
>>> del Class.data  # (5)
>>> obj.data  # (6)
'bar'
  1. obj.data obtém o atributo de instância data.

  2. Class.data obtém o atributo de classe data.

  3. Sobrescreve Class.data com uma nova propriedade.

  4. obj.data está agora ocultado pela propriedade Class.data.

  5. Apaga a propriedade .

  6. obj.data agora lê novamente o atributo de instância data.

O ponto principal desta seção é que uma expressão como obj.data não começa a busca por data em obj. A busca na verdade começa em obj.__class__, e o Python só olha para a instância obj se não houver uma propriedade chamada data na classe. Isso se aplica a descritores dominantes em geral, dos quais as propriedades são apenas um exemplo. Mas um tratamento mais profundo de descritores vai ter que aguardar pelo [attribute_descriptors].

Voltemos às propriedades. Toda unidade de código do Python—módulos, funções, classes, métodos—pode conter uma docstring. O próximo tópico mostra como anexar documentação às propriedades.

Documentação de propriedades

Quando ferramentas como a função help() do console ou IDEs precisam mostrar a documentação de uma propriedade, elas extraem a informação do atributo __doc__ da propriedade.

Se usada com a sintaxe clássica de invocação, property pode receber a string de documentação no argumento doc:

    weight = property(get_weight, set_weight, doc='weight in kilograms')

A docstring do método getter—aquele que recebe o decorador @property—é usado como documentação da propriedade toda. O Figura 1 mostra telas de ajuda geradas a partir do código no Exemplo 26.

Screenshots of the Python console
Figura 1. Capturas de tela do console do Python para os comandos help(Foo.bar) e help(Foo). O código-fonte está no Exemplo 26.
Exemplo 26. Documentação para uma propriedade
link:code/22-dyn-attr-prop/doc_property.py[role=include]

Agora que cobrimos o essencial sobre as propriedades, vamos voltar para a questão de proteger os atributos weight e price de LineItem, para que eles só aceitem valores maiores que zero—mas sem implementar manualmente dois pares de getters/setters praticamente idênticos.

Criando uma fábrica de propriedades

Vamos programar uma fábrica para criar propriedades quantity (quantidade)--assim chamadas porque os atributos gerenciados representam quantidades que não podem ser negativas ou zero na aplicação. O Exemplo 27 mostra a aparência cristalina da classe LineItem usando duas instâncias de propriedades quantity: uma para gerenciar o atributo weight, a outra para o price.

Exemplo 27. bulkfood_v2prop.py: a fábrica de propriedades quantity em ação
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
  1. Usa a fábrica para definir a primeira propriedade personalizada, weight, como um atributo de classe.

  2. Essa segunda chamada cria outra propriedade personalizada, price.

  3. Aqui a propriedade já está ativa, assegurando que um peso negativo ou 0 seja rejeitado.

  4. As propriedades também são usadas aqui, para recuperar os valores armazenados na instância.

Recorde que propriedades são atributos de classe. Ao criar cada propriedade quantity, precisamos passar o nome do atributo de LineItem que será gerenciado por aquela propriedade específica. Ter que digitar a palavra weight duas vezes na linha abaixo é lamentável:

    weight = quantity('weight')

Mas evitar tal repetição é complicado, pois a propriedade não tem como saber qual nome de atributo será vinculado a ela. Lembre-se: o lado direito de uma atribuição é avaliado primeiro, então quando quantity() é invocada, o atributo de classe weight sequer existe.

Note

Aperfeiçoar a propriedade quantity para que o usuário não precise redigitar o nome do atributo é uma problema não-trivial de metaprogramação. Um problema que resolveremos no [attribute_descriptors].

O Exemplo 28 apresenta a implementação da fábrica de propriedades quantity.[11]

Exemplo 28. bulkfood_v2prop.py: a fábrica de propriedades quantity
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
  1. O argumento storage_name, onde os dados de cada propriedade são armazenados; para weight, o nome do armazenamento será 'weight'.

  2. O primeiro argumento do qty_getter poderia se chamar self, mas soaria estranho, pois isso não é o corpo de uma classe; instance se refere à instância de LineItem onde o atributo será armazenado.

  3. qty_getter se refere a storage_name, então ele será preservado na clausura desta função; o valor é obtido diretamente de instance.__dict__, para contornar a propriedade e evitar uma recursão infinita.

  4. qty_setter é definido, e também recebe instance como primeiro argumento.

  5. O value é armazenado diretamente no instance.__dict__, novamente contornando a propriedade.

  6. Cria e devolve um objeto propriedade personalizado.

As partes do Exemplo 28 que merecem um estudo mais cuidadoso giram em torno da variável storage_name.

Quando programamos um propriedade da maneira tradicional, o nome do atributo onde um valor será armazenado está definido explicitamente nos métodos getter e setter. Mas aqui as funções qty_getter e qty_setter são genéricas, e dependem da variável storage_name para saber onde ler/escrever o atributo gerenciado no __dict__ da instância. Cada vez que a fábrica quantity é chamada para criar uma propriedade, storage_name precisa ser definida com um valor único.

As funções qty_getter e qty_setter serão encapsuladas pelo objeto property, criado na última linha da função fábrica. Mais tarde, quando forem chamadas para cumprir seus papéis, essas funções lerão a storage_name de suas clausuras para determinar de onde ler ou onde escrever os valores dos atributos gerenciados.

No Exemplo 29, criei e inspecionei uma instância de LineItem, expondo os atributos armazenados.

Exemplo 29. bulkfood_v2prop.py: explorando propriedades e atributos de armazenamento
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
  1. Lendo o weight e o price através das propriedades que ocultam os atributos de instância de mesmo nome.

  2. Usando vars para inspecionar a instância nutmeg: aqui vemos os reais atributos de instância usados para armazenar os valores.

Observe como as propriedades criadas por nossa fábrica se valem do comportamento descrito na Propriedades sobrepõe atributos de instância: a propriedade weight se sobrepõe ao atributo de instância weight, de forma que qualquer referência a self.weight ou nutmeg.weight é tratada pelas funções da propriedade, e a única maneira de contornar a lógica da propriedade é acessando diretamente o `__dict__`da instância.

O código no Exemplo 28 pode ser um pouco complicado, mas é conciso: seu tamanho é idêntico ao do par getter/setter decorado que define apenas a propriedade weight no Exemplo 21. A definição de LineItem no Exemplo 27 parece muito melhor sem o ruído de getters e setters.

Em um sistema real, o mesmo tipo de validação pode aparecer em muitos campos espalhados por várias classes, e a fábrica quantity estaria em um módulo utilitário, para ser usada continuamente. Por fim, aquela fábrica simples poderia ser refatorada em um classe descritora mais extensível, com subclasses especializadas realizando diferentes validações. Faremos isso no [attribute_descriptors].

Vamos agora encerrar a discussão das propriedades com a questão da exclusão de atributos.

Tratando a exclusão de atributos

Podemos usar a instrução del para excluir não apenas variáveis, mas também atributos:

>>> class Demo:
...    pass
...
>>> d = Demo()
>>> d.color = 'green'
>>> d.color
'green'
>>> del d.color
>>> d.color
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Demo' object has no attribute 'color'

Na prática, a exclusão de atributos não é algo que se faça todo dia no Python, e a necessidade de lidar com isso no caso de uma propriedade é ainda mais rara. Mas tal operação é suportada, e consigo pensar em um exemplo bobo para demonstrá-la.

Em uma definição de propriedade, o decorador @my_property.deleter encapsula o método responsável por excluir o atributo gerenciado pela propriedade. Como prometido, o tolo Exemplo 30 foi inspirado pela cena com o Cavaleiro Negro, do filme Monty Python e o Cálice Sagrado.[12]

Exemplo 30. blackknight.py
link:code/22-dyn-attr-prop/blackknight.py[role=include]

Os doctests em blackknight.py estão no Exemplo 31.

Exemplo 31. blackknight.py: doctests para Exemplo 30 (o Cavaleiro Negro nunca reconhece a derrota)
link:code/22-dyn-attr-prop/blackknight.py[role=include]

Usando a sintaxe clássica de invocação em vez de decoradores, o argumento fdel configura a função de exclusão. Por exemplo, a propriedade member seria escrita assim no corpo da classe BlackKnight:

    member = property(member_getter, fdel=member_deleter)

Se você não estiver usando uma propriedade, a exclusão de atributos pode ser tratada implementando o método especial de nível mais baixo __delattr__, apresentado na Métodos especiais para tratamento de atributos. Programar um classe tola com __delattr__ fica como exercício para a leitora que queira procrastinar.

Propriedades são recursos poderosos, mas algumas vezes alternativas mais simples ou de nível mais baixo são preferíveis. Na seção final deste capítulo, vamos revisar algumas das APIs essenciais oferecidas pelo Python para programação de atributos dinâmicos.

Atributos e funções essenciais para tratamento de atributos

Por todo este capítulo, e mesmo antes no livro, usamos algumas das funções embutidas e alguns dos métodos especiais oferecidos pelo Python para lidar com atributos dinâmicos. Esta seção os reúne em um único lugar para uma visão geral, pois sua documentação está espalhada na documentação oficial.

Atributos especiais que afetam o tratamento de atributos

O comportamento de muitas das funções e dos métodos especiais elencados nas próximas seções dependem de três atributos especiais:

__class__

Uma referência à classe do objeto (isto é, obj.__class__ é o mesmo que type(obj)). O Python procura por métodos especiais tal como __getattr__ apenas na classe do objeto, e não nas instâncias em si.

__dict__

Um mapeamento que armazena os atributos passíveis de escrita de um objeto ou de uma classe. Um objeto que tenha um __dict__ pode ter novos atributos arbitrários definidos a qualquer tempo. Se uma classe tem um atributo __slots__, então suas instâncias não podem ter um __dict__. Veja __slots__ (abaixo).

__slots__

Um atributo que pode ser definido em uma classe para economizar memória. __slots__ é uma tuple de strings, nomeando os atributos permitidos[13]. Se o nome '__dict__' não estiver em __slots__, as instâncias daquela classe então não terão um __dict__ próprio, e apenas os atributos listados em __slots__ serão permitidos naquelas instâncias. Revise a [slots_section] para recordar esse tópico.

Funções embutidas para tratamento de atributos

Essas cinco funções embutidas executam leitura, escrita e introspecção de atributos de objetos:

dir([object])

Lista a maioria dis atributos de um objeto. A documentação oficial diz que o objetivo de dir é o uso interativo, então ele não fornece uma lista completa de atributos, mas um conjunto de nomes "interessantes". dir pode inspecionar objetos implementados com ou sem um __dict__. O próprio atributo __dict__ não é exibido por dir, mas as chaves de __dict__ são listadas. Vários atributos especiais de classes, tais como __mro__, __bases__ e __name__, também não são exibidos por dir. Você pode personalziar a saída de dir implementando o método especial __dir__, como vimos no Exemplo 4. Se o argumento opcional object não for passado, dir lista os nomes no escopo corrente.

getattr(object, name[, default])

Devolve o atributo do object identificado pela string name. O principal caso de uso é obter atributos (ou métodos) cujos nomes não sabemos de antemão. Essa função pode recuperar um atributo da classe do objeto ou de uma superclasse. Se tal atributo não existir, getattr gera uma AttributeError ou devolve o valor default, se ele for passado. Um ótimo exemplo de uso de gettatr aparece no método Cmd.onecmd, no pacote cmd da biblioteca padrão, onde ela é usada para obter e executar um comando definido pelo usuário.

hasattr(object, name)

Devolve True se o atributo nomeado existir em object, ou puder ser obtido de alguma forma através dele (por herança, por exemplo). A documentação explica: "Isto é implementado chamando getattr(object, name) e vendo se [isso] levanta um AttributeError ou não."

setattr(object, name, value)

Atribui o value ao atributo de object nomeado, se o object permitir essa operação. Isso pode criar um novo atributo ou sobrescrever um atributo existente.

vars([object])

Devolve o __dict__ de object; vars não funciona com instâncias de classes que definem __slots__ e não têm um __dict__ (compare com dir, que aceita essas instâncias). Sem argumentos, vars() faz o mesmo que locals(): devolve um dict representando o escopo local.

Métodos especiais para tratamento de atributos

Quando implementados em uma classe definida pelo usuário, os métodos especiais listados abaixo controlam a recuperação, a atualização, a exclusão e a listagem de atributos.

Acessos a atributos, usando tanto a notação de ponto ou as funções embutidas getattr, hasattr e setattr disparam os métodos especiais adequados, listados aqui. A leitura e escrita direta de atributos no __dict__ da instância não dispara esses métodos especiais—​e essa é a forma habitual de evitá-los se isso for necessário.

A seção "3.3.11. Pesquisa de método especial" do capítulo "Modelo de dados" adverte:

Para classes personalizadas, as invocações implícitas de métodos especiais só têm garantia de funcionar corretamente se definidas em um tipo de objeto, não no dicionário de instância do objeto.

Em outras palavras, assuma que os métodos especiais serão acessados na própria classe, mesmo quando o alvo da ação é uma instância. Por essa razão, métodos especiais não são ocultados por atributos de instância de mesmo nome.

Nos exemplos a seguir, assuma que há uma classe chamada Class, que obj é uma instância de Class, e que attr é um atributo de obj.

Para cada um destes métodos especiais, não importa se o acesso ao atributo é feito usando a notação de ponto ou uma das funções embutidas listadas acima, em Funções embutidas para tratamento de atributos. Por exemplo, tanto obj.attr quanto getattr(obj, 'attr', 42) disparam Class.__getattribute__(obj, 'attr').

__delattr__(self, name)

É sempre invocado quando ocorre uma tentativa de excluir um atributo usando a instrução del; por exemplo, del obj.attr dispara Class.__delattr__(obj, 'attr'). Se attr for uma propriedade, seu método de exclusão nunca será invocado se a classe implementar __delattr__.

__dir__(self)

Chamado quando dir é invocado sobre um objeto, para fornecer uma lista de atributos; por exemplo, dir(obj) dispara Class.__dir__(obj). Também usado pelo recurso de auto-completar em todos os consoles modernos do Python.

__getattr__(self, name)

Chamado apenas quando uma tentativa de obter o atributo nomeado falha, após obj, Class e suas superclasses serem pesquisadas. As expressões obj.no_such_attr, getattr(obj, 'no_such_attr') e hasattr(obj, 'no_such_attr') podem disparar Class.__getattr__(obj, 'no_such_attr'), mas apenas se um atributo com aquele nome não for encontrado em obj ou em Class e suas superclasses.

__getattribute__(self, name)

Sempre chamado quando há uma tentativa de obter o atributo nomeado diretamente a partir de código Python (o interpretador pode ignorar isso em alguns casos, por exemplo para obter o método __repr__). A notação de ponto e as funções embutidas getattr e hasattr disparam esse método. __getattr__ só é invocado após __getattribute__, e apenas quando __getattribute__ gera uma AttributeError. Para acessar atributos da instância obj sem entrar em uma recursão infinita, implementações de __getattribute__ devem usar super().__getattribute__(obj, name).

__setattr__(self, name, value)

Sempre chamado quando há uma tentativa de atribuir um valor ao atributo nomeado. A notação de ponto e a função embutida setattr disparam esse método; por exemplo, tanto obj.attr = 42 quanto setattr(obj, 'attr', 42) disparam Class.__setattr__(obj, 'attr', 42).

Warning

Na prática, como são chamados incondicionalmene e afetam praticamente todos os acessos a atributos, os métodos especiais __getattribute__ e __setattr__ são mais difíceis de usar corretamente que __getattr__, que só lida com nome de atributos não-existentes. Usar propriedades ou descritores tende a causar menos erros que definir esses métodos especiais.

Isso conclui nosso mergulho nas propriedades, nos métodos especiais e nas outras técnicas de programação de atributos dinâmicos.

Resumo do capítulo

Começamos nossa discussão dos atributos dinâmicos mostrando exemplos práticos de classes simples, que tornavam mais fácil processar um conjunto de dados JSON. O primeiro exemplo foi a classe FrozenJSON, que converte listas e dicts aninhados em instâncias aninhadas de FrozenJSON, e em listas de instâncias da mesma classe. O código de FrozenJSON demonstrou o uso do método especial __getattr__ para converter estruturas de dados em tempo real, sempre que seus atributos eram lidos. A última versão de FrozenJSON mostrou o uso do método construtor __new__ para transformar uma classe em uma fábrica flexível de objetos, não restrita a instâncias de si mesma.

Convertemos então o conjunto de dados JSON em um dict que armazena instâncias da classe Record. A primeira versão de Record tinha apenas algumas linhas e introduziu o dialeto do "punhado" ("bunch"): usar self.__dict__.update(**kwargs) para criar atributos arbitrários a partir de argumentos nomeados passados para __init__. A segunda passagem acrescentou a classe Event, implementando a recuperação automática de registros relacionados através de propriedades. Valores calculados de propriedades algumas vezes exigem caching, e falamos de algumas formas de fazer isso. Após descobrir que @functools.cached_property não é sempre aplicável, aprendemos sobre uma alternativa: a combinação de @property acima de @functools.cache, nessa ordem.

A discussão sobre propriedades continuou com a classe LineItem, onde uma propriedade foi criada para proteger um atributo weight de receber valores negativos ou zero, que não fazem sentido em termos do negócio. Após um aprofundamento da sintaxe e da semântica das propriedades, criamos uma fábrica de propriedades para aplicar a mesma validação a weight e a price, sem precisar escrever múltiplos getters e setters. A fábrica de propriedades se apoiou em conceitos sutis—tais como clausuras e a sobreposição de atributos de instância por propriedades—​para fornecer um solução genérica elegante, usando para isso o mesmo número de linhas que usamos antes para escrever manualmente a definição de uma única propriedade.

Por fim, demos uma rápida passada pelo tratamento da exclusão de atributos com propriedades, seguida por um resumo dos principais atributos especiais, funções embutidas e métodos especiais que suportam a metaprogramação de atributos no núcleo da linguagem Python.

Leitura Complementar

A documentação oficial para as funções embutidas de tratamento de atributos e introspecção é o Capítulo 2, "Funções embutidas" da Biblioteca Padrão do Python. Os métodos especiais relacionados e o atributo especial __slots__ estão documentados em A Referência da Linguagem Python, em "3.3.2. Personalizando o acesso aos atributos". A semântica de como métodos especiais são invocados ignorando as instâncias está explicada em "3.3.11. Pesquisa de método especial". No capítulo 4 da Biblioteca Padrão do Python, "Tipos embutidos", "Atributos especiais" trata dos atributos __class__ e __dict__.

O Python Cookbook (EN), 3ª ed., de David Beazley e Brian K. Jones (O’Reilly), tem várias receitas relacionadas aos tópicos deste capítulo, mas eu destacaria três mais marcantes: A "Recipe 8.8. Extending a Property in a Subclass" (Receita 8.8. Estendendo uma Propriedade em uma Subclasse) trata da espinhosa questão de sobrepor métodos dentro de uma propriedade herdada de uma superclasse; a "Recipe 8.15. Delegating Attribute Access" (Receita 8.15. Delegando o Acesso a Atributos) implementa uma classe proxy, demonstrando a maioria dos métodos especiais da Métodos especiais para tratamento de atributos deste livro; e a fantástica "Recipe 9.21. Avoiding Repetitive Property Methods" (Receita 9.21. Evitando Métodos de Propriedade Repetitivos), que foi a base da função fábrica de propriedades apresentada no Exemplo 28.

O Python in a Nutshell, 3ª ed., de Alex Martelli, Anna Ravenscroft e Steve Holden (O’Reilly), é rigoroso e objetivo. Eles dedicam apenas três páginas a propriedades, mas isso se dá porque o livro segue um estilo de apresentação axiomático: as 15 ou 16 páginas precedentes fornecem uma descrição minuciosa da semântica das classes do Python, a partir do zero, incluindo descritores, que são como as propriedades são efetivamente implementadas debaixo dos panos. Assim, quando Martelli et al. chegam à propriedades, eles concentram várias ideias profundas naquelas três páginas—incluindo o trecho que selecionei para abrir este capítulo.

Bertrand Meyer—citado na definição do Princípio do Acesso Uniforme no início do capítulo—foi um pioneiro da metodologia Programação por Contrato (Design by Contract), projetou a linguagem Eiffel e escreveu o excelente Object-Oriented Software Construction, 2ª ed. (Pearson). Os primeiros seis capítulos fornecem uma das melhores introduções conceituais à análise e design orientados a objetos que tenho notícia. O capítulo 11 apresenta a Programação por Contrato, e o capítulo 35 traz as avaliações de Meyer de algumas das mais influentes linguagens orientadas a objetos: Simula, Smalltalk, CLOS (the Common Lisp Object System), Objective-C, C++, e Java, com comentários curtos sobre algumas outras. Apenas na última página do livro o autor revela que a "notação" extremamente legível usada como pseudo-código no livro é Eiffel.

Ponto de Vista

O Princípio de Acesso Uniforme de Meyer é esteticamente atraente. Como um programador usando uma API, eu não deveria ter de me preocupar se product.price simplesmente lê um atributo de dados ou executa uma computação. Como um consumidor e um cidadão, eu me importo: no comércio online atual, o valor de product.price muitas vezes depende de quem está perguntando, então ele certamente não é um mero atributo de dados. Na verdade, é uma prática comum apresentar um preço mais baixo se a consulta vem de fora da loja—por exemplo, de um mecanismo de comparação de preços. Isso efetivamente pune os fregueses fiéis, que gostam de navegar dentro de uma loja específica. Mas estou divagando.

A digressão anterior toca um ponto relevante para programação: apesar do Princípio de Acesso Uniforme fazer todo sentido em um mundo ideal, na realidade os usuários de uma API podem precisar saber se ler product.price é potencialmente dispendioso ou demorado demais. Isso é um problema com abstrações de programaçõa em geral: elas tornam difícil raciocinar sobre o custo da avaliação de uma expressão durante a execução. Por outro lado, abstrações permitem aos usuários fazerem mais com menos código. É uma negociação. Como de hábito em questões de engenharia de software, o wiki original (EN) de Ward Cunningham contém argumentos perspicazes sobre os méritos do Princípio de Acesso Uniforme (EN).

Em linguagens de programação orientadas a objetos, a aplicação ou violação do Princípio de Acesso Uniforme muitas vezes gira em torno da sintaxe de leitura de atributos de dados públicos versus a invocação de métodos getter/setter.

Smalltalk e Ruby resolvem essa questão de uma forma simples e elegante: elas não suportam nenhuma forma de atributos de dados públicos. Todo atributo de instância nessas linguagens é privado, então qualquer acesso a eles deve passar por métodos. Mas sua sintaxe torna isso indolor: em Ruby, product.price invoca o getter de price; em Smalltalk, ele é simplesmente product price.

Na outra ponta do espectro, a linguagem Java permite ao programador escolher entre quatro modificadores de nível de acesso—incluindo o default sem nome que o Tutorial do Java (EN) chama de "package-private".

A prática geral, entretanto, não concorda com a sintaxe definida pelos projetistas do Java. Todos no campo do Java concordam que atributos devem ser private, e é necessário escrever isso explicitamente todas as vezes, porque não é o default. Quando todos os atributos são privados, todo acesso a eles de fora da classe precisa passar por métodos de acesso. Os IDEs de Java incluem atalhos para gerar métodos de acesso automaticamente. Infelizmente, o IDE não ajuda quando você precisa ler aquele código seis meses depois. É problema seu navegar por um oceano de métodos de acesso que não fazem nada, para encontrar aqueles que adicionam valor, implementando alguma lógica do negócio.

Alex Martelli fala pela maioria da comunidade Python quando chama métodos de acesso de "idiomas patéticos", e então apresenta esse exemplos que parecem muito diferentes, mas fazem a mesma coisa: [14]

someInstance.widgetCounter += 1
# rather than...
someInstance.setWidgetCounter(someInstance.getWidgetCounter() + 1)

Algumas vezes, ao projetar uma API, me pergunto se todo método que não recebe qualquer argumento (além de self), devolve um valor (diferente de None) e é uma função pura (isto é, não tem efeitos colaterais) não deveria ser substituído por uma propriedade somente de leitura. Nesse capítulo, o método LineItem.subtotal (no Exemplo 27) seria um bom candidato a se tornar uma propriedade somente para leitura. Claro, isso exclui métodos projetados para modificar o objeto, tal como my_list.clear(). Seria uma péssima ideia transformar isso em uma propriedade, de tal forma que o mero acesso a my_list.clear apagaria o conteúdo da lista!

Na biblioteca GPIO Pingo (mencionada na [missing_method]), da qual sou co-autor, grande parte da API do usuário está baseada em propriedades. Por exemplo, para ler o valor atual de uma porta analógica, o usuário escreve pin.value, e definir o modo de uma porta digital é escrito pin.mode = OUT. Por trás da cortina, ler o valor de uma porta analógica ou definir o modo de uma porta digital pode implicar em bastante código, dependendo do driver específico da placa. Decidimos usar propriedades no Pingo porque queríamos que a API fosse confortável de usar até mesmo em ambientes interativos como um Jupyter Notebook, e achamos que pin.mode = OUT é mais fácil para os olhos e para os dedos que pin.set_mode(OUT).

Apesar de achar a solução do Smalltalk e do Ruby mais limpa, acho que a abordagem do Python faz mais sentido que a do Java. Podemos começar simples, programando elementos de dados como atributos públicos, pois sabemos que eles sempre podem ser encapsulados por propriedades (ou descritores, dos quais falaremos no próximo capítulo).

+__new__+ é melhor que new

Outro exemplo do Princípio de Acesso Uniforme (ou uma variante dele) é o fato de chamadas de função e instanciação de objetos usarem a mesma sintaxe no Python: my_obj = foo(), onde foo pode ser uma classe ou qualquer outro invocável.

Outras linguagens, influenciadas pela sintaxe do C++, tem um operador new que faz a instanciação parecer diferente de uma invocação. Na maior parte do tempo, o usuário de uma API não se importa se foo é uma função ou uma classe. Por anos tive a impressão que property era uma função. Para o uso normal, não faz diferença.

Há muitas boas razões para substituir construtores por fábricas. [15] Um motivo popular é limitar o número de instâncias, devolvendo objetos construídps anterioremente (como no padrão de projeto Singleton). Um uso relacionado é fazer caching de uma construção de objeto dispendiosa. Além disso, às vezes é conveniente devolver objetos de tipos diferentes, dependendo dos argumentos passados.

Programar um construtor é simples; fornecer uma fábrica aumenta a flexibilidade às custas de mais código. Em linguagens com um operador new, o projetista de uma API precisa decidir a priori se vai se ater a um construtor simples ou investir em uma fábrica. Se a escolha inicial estiver errada, a correção pode ser cara—tudo porque new é um operador.

Algumas vezes pode ser conveniente pegar o caminho inverso, e substituir uma função simples por uma classe.

Em Python, classes e funções são muitas vezes intercambiáveis. Não apenas pela ausência de um operador new, mas também porque existe o método especial __new__, que pode transformar uma classe em uma fábrica que produz tipos diferentes de objetos (como vimos na Criação flexível de objetos com __new__) ou devolver instâncias pré-fabricadas em vez de criar uma nova todas as vezes.

Essa dualidade função-classe seria mais fácil de aproveitar se a PEP 8 — Style Guide for Python Code (Guia de Estilo para Código Python) não recomendasse CamelCase para nomes de classe. Por outro lado, dezenas de classes na biblioteca padrão tem nomes apenas em minúsculas (por exemplo, property, str, defaultdict, etc.). Daí que talvez o uso de nomes de classe apenas com minúsculas seja um recurso, e não um bug. Mas independente de como olhemos, essa inconsistência no uso de maiúsculas e minúsculas nos nomes de classes na biblioteca padrão do Python nos coloca um problema de usabilidade.

Apesar da invocação de uma função não ser diferente da invocação de uma classe, é bom saber qual é qual, por causa de outra coisa que podemos fazer com uma classe: criar uma subclasse. Então eu, pessoalmente, uso CamelCase em todas as classes que escrevo, e gostaria que todas as classea na biblioteca padrão do Python seguissem a mesma convenção. Estou olhando para vocês, collections.OrderedDict e collections.defaultdict.


1. Alex Martelli, Anna Ravenscroft & Steve Holden, Python in a Nutshell, Third Edition (EN) (O’Reilly), p. 123.
2. Bertrand Meyer, Object-Oriented Software Construction, 2nd ed. (Pearson), p. 57. (EN)
3. A OSCON—O’Reilly Open Source Conference (Conferência O’Reilly de Código Aberto)—foi uma vítima da pandemia de COVID-19. O arquivo JSON original de 744 KB, que usei para esses exemplos, não está mais disponível online hoje (10 de janeiro de 2021). Você pode obter uma cópia do osconfeed.json no repositório de exemplos do livro.
4. Dois exemplos são AttrDict e addict.
5. A expressão self.__data[name] é onde a exceção KeyError pode acontecer. Idealmente, ela deveria ser tratada, e uma AttributeError deveria ser gerada em seu lugar, pois é isso que se espera de __getattr__. O leitor mais diligente está convidado a programar o tratamento de erro, como um exercício.
6. A fonte dos dados é JSON e os únicos tipos de coleção em dados JSON são dict e list.
7. Bunch ou "punhado" é o nome da classe usada por Alex Martelli para compartilhar essa dica em uma receita de 2001 intitulada "The simple but handy ‘collector of a bunch of named stuff’ class" (Uma classe simples mas prática 'coletora de um punhado de coisas nomeadas').
8. Isso é, na verdade, uma desvantagem do Princípio de Acesso Uniforme de Meyer, mencionada no início deste capítulo. Quem tiver interesse nessa discussão pode ler o Ponto de Vista opcional.
9. Fonte: documentação de @functools.cached_property. Sei que autor dessa explicação é Raymond Hettinger porque ele a escreveu em resposta a um problema que eu mesmo reportei: bpo42781—functools.cached_property docs should explain that it is non-overriding (a documentação de functools.cached_property deveria explicar que ele é não-dominante) (EN). Hettinger é um grande colaborador da documentação oficial do Python e da biblioteca padrão. Ele também escreveu o excelente Descriptor HowTo Guide (Guia de Utilização de Descritores) (EN), um recurso fundamental para o [attribute_descriptors].
10. Citação direta de Jeff Bezos no artigo do Wall Street Journal "Birth of a Salesman" (O Nascimento de um Vendedor) (EN) (15 de outubro de 2011). Pelo menos até 2023, é necessario ser assinante para ler o artigo.
11. Esse código foi adaptado da "Recipe 9.21. Avoiding Repetitive Property Methods" (Receita 9.21. Evitando Métodos Repetitivos de Propriedades) do Python Cookbook (EN), 3ª ed., de David Beazley e Brian K. Jones (O’Reilly).
12. Aquela cena sangrenta está disponível no Youtube (EN) quando reviso essa seção, em outubro de 2021.
13. Alex Martelli assinala que, apesar de __slots__ poder ser definido como uma list, é melhor ser explícito e sempre usar uma tuple, pois modificar a lista em __slots__ após o processamento do corpo da classe não tem qualquer efeito. Assim, seria equivocado usar uma sequência mutável ali.
14. Alex Martelli, Python in a Nutshell, 2ª ed. (O’Reilly), p. 101.
15. As razões que menciono foram apresentadas no artigo intitulado "Java’s new Considered Harmful" (new do Java considerado nocivo), de Jonathan Amsterdam, publicado na Dr. Dobbs Journal, e no "Consider static factory methods instead of constructors" (Considere substituir construtores por métodos estáticos de fábrica), que é o Item 1 do premiado livro Effective Java, 3ª ed., de Joshua Bloch (Addison-Wesley).