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]
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]
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.
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.
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]
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.
link:code/22-dyn-attr-prop/oscon/osconfeed_explore.rst[role=include]
-
feed
é umdict
contendo dicts e listas aninhados, com valores string e inteiros. -
Lista as quatro coleções de registros dentro de
"Schedule"
. -
Exibe a contagem de registros para cada coleção.
-
Navega pelos dicts e listas aninhados para obter o nome da última palestrante (
speaker
). -
Obtém o número de série para aquela mesma palestrante.
-
Cada evento tem uma lista
'speakers'
, com o número de série de zero ou mais palestrantes.
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.
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]
-
Cria uma instância de
FrozenJSON
a partir deraw_feed
, feito de dicts e listas aninhados. -
FrozenJSON
permite navegar dicts aninhados usando a notação de atributos; aqui exibimos o tamanho da lista de palestrantes. -
Métodos dos dicts subjacentes também podem ser acessados; por exemplo,
.keys()
, para recuperar os nomes das coleções de registros. -
Usando
items()
, podemos buscar os nomes das coleções de registros e seus conteúdos, para exibir olen()
de cada um deles. -
Uma
list
, tal comofeed.Schedule.speakers
, permanece uma lista, mas os itens dentro dela, se forem mapeamentos, são convertidos em umFrozenJSON
. -
O item 40 na lista
events
era um objeto JSON; agora ele é uma instância deFrozenJSON
. -
Registros de eventos tem uma lista de
speakers
com os números de séries de palestrantes. -
Tentar ler um atributo inexistente gera uma exceção
KeyError
, em vez daAttributeError
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.
FrozenJSON
contendo objetos FrozenJSON
aninhados, listas e tipos simpleslink:code/22-dyn-attr-prop/oscon/explore0.py[role=include]
-
Cria um
dict
a partir do argumentomapping
. 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. -
__getattr__
é invocado apenas quando não existe um atributo com aquelename
. -
Se
name
corresponde a um atributo da instância dedict
__data
, devolve aquele atributo. É assim que chamadas comofeed.keys()
são tratadas: o métodokeys
é um atributo dodict
__data
. -
Caso contrário, obtém o item com a chave
name
deself.__data
, e devolve o resultado da chamadaFrozenJSON.build()
com aquele argumento.[5] -
Implementar
__dir__
suporta a função embutidadir()
, 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 emself.__data
, porque__getattr__
cria instâncias deFrozenJSON
em tempo real—um recurso útil para a exploração interativa dos dados. -
Este é um construtor alternativo, um uso comum para o decorador
-
Se
obj
é um mapeamento, cria umFrozenJSON
com ele. Esse é um exmeplo de goose typing—veja a [goose_typing_sec] caso precise de uma revisão desse tópico. -
Se for uma
MutableSequence
, tem que ser uma lista[6], então criamos umalist
, passando recursivamente cada item emobj
para.build()
. -
Se não for um
dict
ou umalist
, 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 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
_
a nomes de atributo que sejam palavraas reservadas do Pythonlink:code/22-dyn-attr-prop/oscon/explore1.py[role=include]
-
A função
keyword.iskeyword(…)
é exatamente o que precisamos; para usá-la, o módulokeyword
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.
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__
.
__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]
-
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 porself
. -
O comportamento default é delegar para o
__new__
de uma superclasse. Nesse caso, estamos invocando o__new__
da classe baseobject
, passandoFrozenJSON
como único argumento. -
As linhas restantes de
__new__
são exatamente as do antigo métodobuild
. -
Era daqui que
FrozenJSON.build
era chamado antes; agora chamamos apenas a classeFrozenJSON
, e o Python trata essa chamada invocandoFrozenJSON.__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.
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.
venue
e speakers
devolve objetos Record
link:code/22-dyn-attr-prop/oscon/schedule_v4.py[role=include]
-
Dada uma instância de
Event
,… -
…ler
event.venue
devolve um objetoRecord
em vez de um número de série. -
Agora é fácil obter o nome do
venue
. -
A propriedade
event.speakers
devolve uma lista de instâncias deRecord
.
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
.
O Exemplo 8 mostra o doctest para orientar esse primeiro passo.
link:code/22-dyn-attr-prop/oscon/schedule_v1.py[role=include]
-
load
umdict
com os dados JSON. -
As chaves em
records
são strings criadas a partir do tipo de registro e do número de série. -
speaker
é uma instância da classeRecord
, definida no Exemplo 9. -
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.
link:code/22-dyn-attr-prop/oscon/schedule_v1.py[role=include]
-
Isso é um atalho comum para construir uma instância com atributos criados a partir de argumentos nomeados (a explicação detalhada está abaixo) .
-
Usa o campo
serial
para criar a representação personalizada deRecord
exibida no Exemplo 8. -
load
vai por fim devolver umdict
de instâncias deRecord
. -
Analisa o JSON, devolvendo objetos Python nativos: listas, dicts, strings, números, etc.
-
Itera sobre as quatro listas principais, chamadas
'conferences'
,'events'
,'speakers'
, e'venues'
. -
record_type
é o nome da lista sem o último caractere, entãospeakers
se tornaspeaker
. No Python ≥ 3.9, podemos fazer isso de forma mais explícita comcollection.removesuffix('s')
—veja a PEP 616—String methods to remove prefixes and suffixes (Métodos de string para remover prefixos e sufixos_). -
Cria a
key
no formato'speaker.3471'
. -
Cria uma instância de
Record
e a armazena emrecords
com a chavekey
.
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 |
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 |
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.
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.
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
-
O método estático
Record.fetch
obtém umRecord
ou umEvent
do conjunto de dados. -
Observe que
event
é uma instância da classeEvent
. -
Acessar
event.venue
devolve uma instância deRecord
. -
Agora é fácil encontrar o nome de um
event.venue
. -
A instância de
Event
também tem um atributovenue_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.
Record
com um novo método fetch
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
-
inspect
será usado emload
, lista do no Exemplo 13. -
No final, o atributo de classe privado
__index
manterá a referência aodict
devolvido porload
. -
fetch
é umstaticmethod
, para deixar explícito que seu efeito não é influenciado pela classe ou pela instância de onde ele é invocado. -
Preenche o
Record.__index
, se necessário. -
E o utiliza para obter um registro com uma dada
key
.
Tip
|
Esse é um exemplo onde o uso de |
Agora podemos usar a propriedade na classe Event
, listada no Exemplo 12.
Event
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
-
Event
estendeRecord
. -
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__
deRecord
. -
A propriedade
venue
cria umakey
a partir do atributovenue_serial
, e a passa para o método de classefetch
, herdado deRecord
(a razão para usarself.__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.
load
link:code/22-dyn-attr-prop/oscon/schedule_v2.py[role=include]
-
Até aqui, nenhuma mudança em relação ao
load
em schedule_v1.py (do Exemplo 9). -
Muda a primeira letra de
record_type
para maiúscula, para obter um possível nome de classe; por exemplo,'event'
se torna'Event'
. -
Obtém um objeto com aquele nome do escopo global do módulo; se aquele objeto não existir, obtém a classe
Record
. -
Se o objeto recém-obtido é uma classe, e é uma subclasse de
Record
… -
…vincula o nome
factory
a ele. Isso significa quefactory
pode ser qualquer subclasse deRecord
, dependendo dorecord_type
. -
Caso contrário, vincula o nome
factory
aRecord
. -
O loop
for
, que cria akey
e armazena os registros, é o mesmo de antes, exceto que… -
…o objeto armazenado em
records
é construído porfactory
, e pode serRecord
ou uma subclasse, comoEvent
, selecionada de acordo com orecord_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
.
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.
speakers
link:code/22-dyn-attr-prop/oscon/schedule_v3.py[role=include]
-
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 à propriedadespeakers
. -
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 |
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.
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.
hasattr
desabilita a otimização de compartilhamento de chaveslink:code/22-dyn-attr-prop/oscon/schedule_v4_hasattr.py[role=include]
-
Se a instância não tem um atributo chamado
__speaker_objs
, obtém os objetosspeaker
e os armazena ali.. -
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.
__init__
para manter a otimização de compartilhamento de chaveslink: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.
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.
@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 deproperty()
. Uma propriedade regular bloqueia a escrita em atributos, a menos que um setter seja definido. Umacached_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 comcached_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
|
|
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.
@property
sobre @cache
link:code/22-dyn-attr-prop/oscon/schedule_v5.py[role=include]
-
A ordem é importante:
@property
vai acima… -
…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.
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.
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.
LineItem
mais simpleslink: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.
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]
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.
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
.
LineItem
com uma propriedade weight
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2.py[role=include]
-
Aqui o setter da propriedade já está em uso, assegurando que nenhuma instância com peso negativo possa ser criada.
-
@property
decora o método getter. -
Todos os métodos que implementam a propriedade compartilham o mesmo nome, do atributo público:
weight
. -
O valor efetivo é armazenado em um atributo privado
__weight
. -
O getter decorado tem um atributo
.setter
, que também é um decorador; isso conecta o getter e o setter. -
Se o valor for maior que zero, definimos o
__weight
privado. -
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.
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.
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2b.py[role=include]
-
Um getter simples.
-
Um setter simples.
-
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 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.
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'
-
Define
Class
com dois atributos de classe: o atributodata
e a propriedadeprop
. -
vars
devolve o__dict__
deobj
, mostrando que ele não tem atributos de instância. -
Ler de
obj.data
obtém o valor deClass.data
. -
Escrever em
obj.data
cria um atributo de instância. -
Inspeciona a instância, para ver o atributo de instância.
-
Ler agora de
obj.data
obtém o valor do atributo da instância. Quanto lido a partir da instânciaobj
, odata
da instância oculta odata
da classe. -
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.
>>> 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'
-
Ler
prop
diretamente deClass
obtém o próprio objeto propriedade, sem executar seu método getter. -
Ler
obj.prop
executa o getter da propriedade. -
Tentar definir um atributo
prop
na instância falha. -
Inserir
'prop'
diretamente emobj.__dict__
funciona. -
Podemos ver que agora
obj
tem dois atributos de instância:data
eprop
. -
Entretanto, ler
obj.prop
ainda executa o getter da propriedade. A propriedade não é ocultada pelo atributo de instância. -
Sobrescrever
Class.prop
destrói o objeto propriedade. -
Agora
obj.prop
obtém o atributo de instância.Class.prop
não é mais uma propriedade, então ela não mais sobrepõeobj.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.
>>> 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'
-
obj.data
obtém o atributo de instânciadata
. -
Class.data
obtém o atributo de classedata
. -
Sobrescreve
Class.data
com uma nova propriedade. -
obj.data
está agora ocultado pela propriedadeClass.data
. -
Apaga a propriedade .
-
obj.data
agora lê novamente o atributo de instânciadata
.
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.
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.
help(Foo.bar)
e help(Foo)
. O código-fonte está no Exemplo 26.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.
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
.
quantity
em açãolink:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
-
Usa a fábrica para definir a primeira propriedade personalizada,
weight
, como um atributo de classe. -
Essa segunda chamada cria outra propriedade personalizada,
price
. -
Aqui a propriedade já está ativa, assegurando que um peso negativo ou
0
seja rejeitado. -
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 |
O Exemplo 28 apresenta a implementação da fábrica de propriedades quantity
.[11]
quantity
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
-
O argumento
storage_name
, onde os dados de cada propriedade são armazenados; paraweight
, o nome do armazenamento será'weight'
. -
O primeiro argumento do
qty_getter
poderia se chamarself
, mas soaria estranho, pois isso não é o corpo de uma classe;instance
se refere à instância deLineItem
onde o atributo será armazenado. -
qty_getter
se refere astorage_name
, então ele será preservado na clausura desta função; o valor é obtido diretamente deinstance.__dict__
, para contornar a propriedade e evitar uma recursão infinita. -
qty_setter
é definido, e também recebeinstance
como primeiro argumento. -
O
value
é armazenado diretamente noinstance.__dict__
, novamente contornando a propriedade. -
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.
link:code/22-dyn-attr-prop/bulkfood/bulkfood_v2prop.py[role=include]
-
Lendo o
weight
e oprice
através das propriedades que ocultam os atributos de instância de mesmo nome. -
Usando
vars
para inspecionar a instâncianutmeg
: 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.
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]
link:code/22-dyn-attr-prop/blackknight.py[role=include]
Os doctests em blackknight.py estão no Exemplo 31.
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.
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.
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 quetype(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__
é umatuple
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.
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 pordir
, 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 pordir
. Você pode personalziar a saída dedir
implementando o método especial__dir__
, como vimos no Exemplo 4. Se o argumento opcionalobject
não for passado,dir
lista os nomes no escopo corrente. getattr(object, name[, default])
-
Devolve o atributo do
object
identificado pela stringname
. 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 umaAttributeError
ou devolve o valordefault
, se ele for passado. Um ótimo exemplo de uso degettatr
aparece no métodoCmd.onecmd
, no pacotecmd
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 emobject
, 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 deobject
nomeado, se oobject
permitir essa operação. Isso pode criar um novo atributo ou sobrescrever um atributo existente. vars([object])
-
Devolve o
__dict__
deobject
;vars
não funciona com instâncias de classes que definem__slots__
e não têm um__dict__
(compare comdir
, que aceita essas instâncias). Sem argumentos,vars()
faz o mesmo quelocals()
: devolve umdict
representando o escopo local.
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
disparaClass.__delattr__(obj, 'attr')
. Seattr
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)
disparaClass.__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õesobj.no_such_attr
,getattr(obj, 'no_such_attr')
ehasattr(obj, 'no_such_attr')
podem dispararClass.__getattr__(obj, 'no_such_attr')
, mas apenas se um atributo com aquele nome não for encontrado emobj
ou emClass
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 embutidasgetattr
ehasattr
disparam esse método.__getattr__
só é invocado após__getattribute__
, e apenas quando__getattribute__
gera umaAttributeError
. Para acessar atributos da instânciaobj
sem entrar em uma recursão infinita, implementações de__getattribute__
devem usarsuper().__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, tantoobj.attr = 42
quantosetattr(obj, 'attr', 42)
disparamClass.__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 |
Isso conclui nosso mergulho nas propriedades, nos métodos especiais e nas outras técnicas de programação de atributos dinâmicos.
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.
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.
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
.
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.
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').
__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.