Python @property
A lojinha da Ana tem uma regra simples: preço negativo não existe. Mas, como você viu na aula Encapsulamento, qualquer linha do programa pode escrever cafe.preco = -5, e o Python aceita calado. Uma saída é esconder o preço em _preco e obrigar todo mundo a usar métodos como ver_preco() e mudar_preco(…), mas aí o código fica cheio de parênteses e quem já escrevia cafe.preco tem de mudar tudo. O @property junta o melhor dos dois mundos: por fora, cafe.preco continua parecendo um atributo comum; por dentro, cada leitura e cada mudança passam por um método seu, que pode calcular, conferir e recusar.
- A criar um atributo calculado com
@property:bia.media, sem parênteses. - Por que um
@propertysozinho é só de leitura, e a mensagemhas no setter. - A escrever o setter (
@preco.setter), que confere o valor e levantaValueErrorquando ele é inválido. - A fazer a validação valer também na criação do objeto, no
__init__. - A tratar o erro com
try/except. - Os erros reais: o
RecursionErrordoself.precodentro do próprio property, os parênteses a mais, o setter com outro nome e o@propertyesquecido.
Esta aula usa duas coisas de aulas anteriores: o raise, que levanta um erro de propósito (aula raise), e o try/except, que trata um erro (aula try e except). O @ antes do nome é um decorador; por enquanto, basta saber que ele "enfeita" o método logo abaixo e muda o jeito de usá-lo. Os decoradores têm aula própria: Decoradores.
A ideia: a máquina de etiquetar
Para pôr preço num produto, a Ana usa a máquina de etiquetar da loja. Para quem olha a prateleira, a etiqueta é só um adesivo com um número. Mas a máquina tem um segredo: antes de imprimir, ela confere o número. Se alguém digita -5, ela apita e não imprime nada.
O @property é essa máquina. Quem usa o objeto lê e escreve cafe.preco como se fosse um adesivo comum. Mas, entre a pessoa e o dado guardado, existe um método que confere tudo.
| Quem usa escreve | O Python chama | Nome em inglês |
|---|---|---|
cafe.preco (ler) | O método marcado com @property | getter, "pegador" |
cafe.preco = 21.00 (mudar) | O método marcado com @preco.setter | setter, "definidor" |
1. O problema
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
cafe = Produto("Café", 18.90)
cafe.preco = -5
print(cafe.preco)
$ python3 problema.py -5
Preço negativo, e ninguém reclamou. Vamos resolver em dois passos: primeiro o @property para ler, depois o setter para mudar com conferência.
2. Um atributo calculado: @property
Comece pelo caso mais simples. A média de uma aluna não precisa ser guardada: ela pode ser calculada a partir das notas toda vez que alguém pedir. Com @property, o cálculo fica num método, mas se usa como atributo:
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
@property
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
print(bia.media)
bia.nota2 = 10.0
print(bia.media)
- @property
- Escrito na linha de cima do
def, colado nele. Transforma o métodomedianuma propriedade: um "atributo" que, quando lido, roda o método. - def media(self):
- Um método comum, só com o
self, que devolve um valor comreturn. - bia.media
- Sem parênteses. Quem lê o código nem percebe que é um método: parece um atributo como
bia.nome. Por baixo, o Python chama omedia(bia).
$ python3 getter.py 8.75 9.0 « a nota mudou, e a média acompanhou sozinha
A vantagem de calcular na hora: a média nunca fica desatualizada. Na aula __init__ e self, a média era calculada uma vez, no __init__; se a nota mudasse depois, a média ficava errada. Com @property, ela é sempre a conta das notas de agora.
Só de leitura
Um @property sozinho só sabe ler. Tentar escrever nele dá erro:
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
@property
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
bia.media = 10
$ python3 sem_setter.py
Traceback (most recent call last):
File "/home/ana/sem_setter.py", line 13, in <module>
bia.media = 10
^^^^^^^^^
AttributeError: property 'media' of 'Aluno' object has no setter
"A propriedade media do objeto Aluno não tem setter": não existe um método para mudar a média. E faz sentido: ninguém muda a média direto; muda as notas. Isso é útil de propósito. Uma conta bancária pode mostrar o saldo como propriedade só de leitura, e deixar o saldo mudar apenas pelos métodos:
class ContaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self._saldo = saldo
@property
def saldo(self):
return self._saldo
def depositar(self, valor):
if valor > 0:
self._saldo += valor
conta = ContaBancaria("Ana", 100)
conta.depositar(50)
print(conta.saldo)
conta.saldo = 1_000_000
$ python3 saldo_leitura.py
150
Traceback (most recent call last):
File "/home/ana/saldo_leitura.py", line 18, in <module>
conta.saldo = 1_000_000
^^^^^^^^^^^
AttributeError: property 'saldo' of 'ContaBancaria' object has no setter
Compare com a aula de encapsulamento: lá, a "fraude" conta.saldo = … passava calada (criava um atributo novo). Aqui ela dá erro, na hora e na linha certa. O valor de verdade fica em self._saldo (com sublinhado, como manda o combinado), e o conta.saldo é a janela de vidro por onde se olha.
Missão: o código lá de baixo usa bia.media sem parênteses, como um atributo, e quebra. Sem mexer nas linhas de baixo, transforme o método media numa propriedade, para o programa mostrar Média da Bia: 8.75.
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
print(f"Média da {bia.nome}: {bia.media:.2f}")
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
@property
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
print(f"Média da {bia.nome}: {bia.media:.2f}")
3. O setter: mudar com conferência
Agora a máquina de etiquetar completa. O preço vai ser lido e mudado como atributo, e toda mudança passa pela conferência:
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, valor):
if valor < 0:
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
self._preco = valor
cafe = Produto("Café", 18.90)
print(cafe.preco)
cafe.preco = 21.00
print(cafe.preco)
print(vars(cafe))
cafe.preco = -5
print("esta linha não roda")
- @property def preco(self):
- O getter: quando alguém lê
cafe.preco, devolve o valor guardado emself._preco. - self._preco
- Onde o valor mora de verdade. O nome precisa ser diferente do da propriedade; o costume é o mesmo nome com um sublinhado na frente. (Veja em Quando dá errado o que acontece se for igual.)
- @preco.setter
- "Este é o setter da propriedade
preco". Oprecodo@preco.setteré a propriedade criada logo acima. - def preco(self, valor):
- O setter tem o mesmo nome da propriedade e recebe, além do
self, ovalorque está do lado direito do=. Emcafe.preco = 21.00, ovaloré21.00. - raise ValueError(…)
- Se o valor não serve, o setter levanta um erro e não guarda nada.
ValueError("erro de valor") é a exceção que o Python usa quando o tipo está certo, mas o valor não faz sentido. - self._preco = valor
- Só chega aqui o valor que passou na conferência.
- self.preco = preco (no __init__)
- Repare: o
__init__escreveself.preco, sem sublinhado. Isso passa pelo setter, e a conferência vale também na criação do produto.
$ python3 setter.py
18.9
21.0
{'nome': 'Café', '_preco': 21.0}
Traceback (most recent call last):
File "/home/ana/setter.py", line 22, in <module>
cafe.preco = -5
^^^^^^^^^^
File "/home/ana/setter.py", line 13, in preco
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
ValueError: preço inválido: -5 (não pode ser negativo)
Linha por linha:
18.9e21.0: a leitura e a mudança funcionaram, como num atributo comum.{'nome': 'Café', '_preco': 21.0}: dentro do objeto, o valor mora em_preco. Não existe nenhum atributoprecono objeto: oprecoé a propriedade, que mora na classe.File "/home/ana/setter.py", line 22, in <module>: o erro começou na linha 22, a docafe.preco = -5.File "/home/ana/setter.py", line 13, in preco: e estourou na linha 13, dentro do setterpreco, noraise.ValueError: preço inválido: -5 (não pode ser negativo): o tipo do erro e a sua mensagem, do jeito que você escreveu. Escreva mensagens que digam o que estava errado e por quê.
E o print("esta linha não roda") não rodou: um erro não tratado encerra o programa.
A conferência vale desde o nascimento
Como o __init__ usa self.preco = preco, nem dá para criar um produto com preço errado:
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, valor):
if valor < 0:
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
self._preco = valor
leite = Produto("Leite", -6.50)
$ python3 no_init.py
Traceback (most recent call last):
File "/home/ana/no_init.py", line 17, in <module>
leite = Produto("Leite", -6.50)
File "/home/ana/no_init.py", line 4, in __init__
self.preco = preco
^^^^^^^^^^
File "/home/ana/no_init.py", line 13, in preco
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
ValueError: preço inválido: -6.5 (não pode ser negativo)
Agora o rastro tem três andares: a linha 17 criou o produto, o __init__ (linha 4) escreveu self.preco, e o setter (linha 13) recusou. Leia de cima para baixo para seguir o caminho; a última linha diz o que deu errado. Se o __init__ escrevesse self._preco = preco, direto, o leite nasceria com preço negativo, sem conferência nenhuma. Por isso: no __init__, use a propriedade, não o atributo com sublinhado.
4. Tratando o erro
Quem usa a classe pode tratar o ValueError com try/except, e o programa segue:
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, valor):
if valor < 0:
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
self._preco = valor
cafe = Produto("Café", 18.90)
for novo in [21.00, -5, 19.90]:
try:
cafe.preco = novo
print(f"Preço trocado para R$ {cafe.preco:.2f}")
except ValueError as erro:
print(f"Não deu: {erro}")
print(f"Preço final: R$ {cafe.preco:.2f}")
$ python3 tratar.py Preço trocado para R$ 21.00 Não deu: preço inválido: -5 (não pode ser negativo) Preço trocado para R$ 19.90 Preço final: R$ 19.90
O -5 foi recusado, o preço anterior (21.00) ficou intacto até a troca seguinte, e o programa não quebrou. O as erro guarda a exceção, e o print dela mostra a mensagem que o setter escreveu.
O setter pode fazer quantas conferências quiser. (Abaixo, só o setter; o resto da classe é igual ao de cima.) Um preço digitado como texto, "18,90", também é um erro, mas de tipo, e para isso existe o TypeError:
@preco.setter
def preco(self, valor):
if not isinstance(valor, (int, float)):
raise TypeError(f"o preço precisa ser um número, não {type(valor).__name__}")
if valor < 0:
raise ValueError(f"preço inválido: {valor} (não pode ser negativo)")
self._preco = valor
Com Produto("Café", "18,90"), a última linha do rastro fica TypeError: o preço precisa ser um número, não str. Sem essa conferência, a comparação valor < 0 daria um erro bem menos claro: TypeError: '<' not supported between instances of 'str' and 'int' ("o < não funciona entre texto e número").
Formas diferentes de proteger o preço
Muita gente que vem do Java escreve métodos get_ e set_:
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.set_preco(preco)
def get_preco(self):
return self._preco
def set_preco(self, valor):
if valor < 0:
raise ValueError("preço negativo")
self._preco = valor
cafe = Produto("Café", 18.90)
cafe.set_preco(cafe.get_preco() + 2)
print(cafe.get_preco())
$ python3 getset.py 20.9
Funciona e valida igual. Mas compare a linha do aumento: cafe.set_preco(cafe.get_preco() + 2) contra cafe.preco += 2, que é o que o @property permite.
| Forma | Quando usar |
|---|---|
Atributo comum: self.preco = preco | Comece por aqui. Se não há regra nenhuma, não precisa de property |
@property sozinho | Valor calculado (media) ou só de leitura (saldo) |
@property + @preco.setter | Quando a mudança precisa de conferência. É o jeito "pythônico" |
get_preco() / set_preco() | Funciona, mas evite em Python: o @property faz o mesmo com uma escrita mais limpa |
A grande vantagem do @property é poder começar simples. Escreva self.preco = preco, um atributo comum. Se um dia aparecer a regra do preço negativo, transforme preco numa propriedade: quem usa a classe continua escrevendo cafe.preco, e nenhuma linha fora da classe precisa mudar.
Quando dá errado
RecursionError
O erro mais comum com @property: guardar o valor com o mesmo nome da propriedade, sem o sublinhado.
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self.preco
@preco.setter
def preco(self, valor):
self.preco = valor
cafe = Produto("Café", 18.90)
$ python3 recursao.py
Traceback (most recent call last):
File "/home/ana/recursao.py", line 15, in <module>
cafe = Produto("Café", 18.90)
~~~~~~~^^^^^^^^^^^^^^^
File "/home/ana/recursao.py", line 4, in __init__
self.preco = preco
^^^^^^^^^^
File "/home/ana/recursao.py", line 12, in preco
self.preco = valor
^^^^^^^^^^
File "/home/ana/recursao.py", line 12, in preco
self.preco = valor
^^^^^^^^^^
File "/home/ana/recursao.py", line 12, in preco
self.preco = valor
^^^^^^^^^^
[Previous line repeated 995 more times]
RecursionError: maximum recursion depth exceeded
Siga o rastro: o __init__ escreveu self.preco, o que chamou o setter. O setter, na linha 12, escreveu self.preco = valor… o que chamou o setter de novo, que escreveu self.preco de novo, e assim por diante. "[Previous line repeated 995 more times]": a mesma linha se repetiu mais 995 vezes, até o Python desistir com RecursionError: maximum recursion depth exceeded, "profundidade máxima de recursão excedida" (aula Recursão). Como consertar: dentro do getter e do setter, use o nome com sublinhado: return self._preco e self._preco = valor. A propriedade é a porta; o _preco é o cômodo.
'float' object is not callable
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
@property
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
print(bia.media())
$ python3 parenteses.py
Traceback (most recent call last):
File "/home/ana/parenteses.py", line 13, in <module>
print(bia.media())
~~~~~~~~~^^
TypeError: 'float' object is not callable
"Um objeto float não pode ser chamado". O bia.media já rodou o método e virou o número 8.75; os parênteses tentaram "chamar" o 8.75. Como consertar: propriedade se usa sem parênteses: bia.media.
@property: unsupported format string passed to method.__format__
O erro contrário: o código usa bia.media sem parênteses, mas a classe esqueceu o @property.
class Aluno:
def __init__(self, nome, nota1, nota2):
self.nome = nome
self.nota1 = nota1
self.nota2 = nota2
def media(self):
return (self.nota1 + self.nota2) / 2
bia = Aluno("Bia", 8.0, 9.5)
print(bia.media)
print(f"Média: {bia.media:.2f}")
$ python3 esqueceu.py
<bound method Aluno.media of <__main__.Aluno object at 0x7f20d53906e0>>
Traceback (most recent call last):
File "/home/ana/esqueceu.py", line 13, in <module>
print(f"Média: {bia.media:.2f}")
^^^^^^^^^^^^^^^
TypeError: unsupported format string passed to method.__format__
O primeiro print mostra o método "amarrado" (o bound method da aula Atributos e Métodos), e não a média. No segundo, o :.2f tentou formatar o método com duas casas decimais, e o Python não sabe fazer isso: "formato não suportado passado para method.__format__". O endereço 0x… muda a cada execução. Como consertar: ponha o @property na linha de cima do def media (ou chame com parênteses, se não quiser uma propriedade).
has no setter logo na criação
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def mudar_preco(self, valor):
if valor < 0:
raise ValueError("preço negativo")
self._preco = valor
cafe = Produto("Café", 18.90)
$ python3 nome_setter.py
Traceback (most recent call last):
File "/home/ana/nome_setter.py", line 17, in <module>
cafe = Produto("Café", 18.90)
~~~~~~~^^^^^^^^^^^^^^^
File "/home/ana/nome_setter.py", line 4, in __init__
self.preco = preco
^^^^^^^^^^
AttributeError: property 'preco' of 'Produto' object has no setter
Parece que o setter está ali, mas o Python diz que a propriedade preco não tem setter. O @preco.setter cria uma propriedade nova, com setter, e a guarda com o nome do def que vem logo abaixo: mudar_preco. A propriedade preco continuou só de leitura. Como consertar: o getter e o setter precisam ter o mesmo nome: def preco(self, valor):.
Missão: o Produto tem a propriedade preco, mas falta o setter, e o programa quebra já na criação do café. Escreva o @preco.setter: ele deve levantar um ValueError quando o valor for negativo e, se não for, guardar o valor em self._preco. O código de baixo tenta pôr -5 (deve aparecer uma linha começando com Erro:) e depois 21.00 (deve aparecer Preço: R$ 21.00).
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
# escreva aqui o setter do preço
cafe = Produto("Café", 18.90)
for novo in [-5, 21.00]:
try:
cafe.preco = novo
except ValueError as erro:
print(f"Erro: {erro}")
print(f"Preço: R$ {cafe.preco:.2f}")
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, valor):
if valor < 0:
raise ValueError("o preço não pode ser negativo")
self._preco = valor
cafe = Produto("Café", 18.90)
for novo in [-5, 21.00]:
try:
cafe.preco = novo
except ValueError as erro:
print(f"Erro: {erro}")
print(f"Preço: R$ {cafe.preco:.2f}")
Referência rápida
| Escrita | O que faz |
|---|---|
@property + def media(self): return … | Cria a propriedade media: lida como obj.media, sem parênteses |
@preco.setter + def preco(self, valor): | O setter: roda em obj.preco = valor |
self._preco | Onde o valor mora de verdade (nome diferente do da propriedade) |
raise ValueError("…") | Recusa um valor que não faz sentido |
raise TypeError("…") | Recusa um valor do tipo errado |
self.preco = preco no __init__ | Faz a conferência valer também na criação |
@preco.deleter | Existe também: roda em del obj.preco. Raramente usado |
| Sintoma | Causa provável |
|---|---|
AttributeError: property 'media' of 'Aluno' object has no setter | Escreveu numa propriedade só de leitura (de propósito ou não), ou o setter tem outro nome |
RecursionError: maximum recursion depth exceeded | O getter ou o setter usa self.preco no lugar de self._preco |
TypeError: 'float' object is not callable | Parênteses numa propriedade: bia.media() |
<bound method …> ou unsupported format string passed to method.__format__ | Faltou o @property |
| Objeto criado com valor inválido, sem erro | O __init__ usa self._preco = … e pula o setter |
Exercícios
1. O decorador
Complete o que vai na linha de cima do def para aluno.media funcionar sem parênteses:
def media(self):
2. O setter
Complete a linha que marca o setter da propriedade preco:
def preco(self, valor):
3. Dentro do setter, onde se guarda o valor?
4. A classe tem @property def saldo(self) e nenhum setter. O que acontece com conta.saldo = 500?
5. Por que o __init__ deve escrever self.preco = preco, e não self._preco = preco?
6. O programa parou com RecursionError: maximum recursion depth exceeded e o rastro repete a linha self.preco = valor. O que está errado?
Missão final: a conta que não aceita saldo negativo
Missão: transforme o saldo da conta numa propriedade com setter. O getter devolve self._saldo; o setter levanta ValueError se o valor for negativo e, se não for, guarda em self._saldo. O código de baixo tenta sacar 500 de quem tem 100 (deve aparecer uma linha começando com Recusado:) e depois sacar 20 (deve sobrar Saldo: R$ 80.00). Não mexa no __init__ nem no sacar: eles já usam self.saldo.
class ContaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
# escreva aqui a propriedade saldo (getter e setter)
def sacar(self, valor):
self.saldo = self.saldo - valor
conta = ContaBancaria("Ana", 100)
for valor in [500, 20]:
try:
conta.sacar(valor)
except ValueError as erro:
print(f"Recusado: {erro}")
print(f"Saldo: R$ {conta.saldo:.2f}")
class ContaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("o saldo não pode ficar negativo")
self._saldo = valor
def sacar(self, valor):
self.saldo = self.saldo - valor
conta = ContaBancaria("Ana", 100)
for valor in [500, 20]:
try:
conta.sacar(valor)
except ValueError as erro:
print(f"Recusado: {erro}")
print(f"Saldo: R$ {conta.saldo:.2f}")
Repare na mágica: o sacar não tem nenhum if. Ele só escreve self.saldo = …, e é o setter quem confere. Qualquer outro método que mexer no saldo (um pagar_boleto, uma transferir) ganha a mesma proteção de graça.
Resumo
@propertytransforma um método numa propriedade: usada como atributo (bia.media, sem parênteses), mas calculada por um método a cada leitura.- Uma propriedade sem setter é só de leitura: escrever nela dá
AttributeError: … has no setter. Ótimo para valores calculados e para o saldo. @preco.setter+def preco(self, valor):roda a cadaobj.preco = valor; ele confere e levantaValueError(ouTypeError) quando o valor não serve.- O valor de verdade mora num nome diferente, com sublinhado:
self._preco. Usarself.precodentro da própria propriedade dáRecursionError. - No
__init__, escrevaself.preco = preco, para a conferência valer desde a criação. - Getter e setter têm o mesmo nome. Comece com atributos comuns; vire propriedade quando aparecer uma regra, sem mudar o código de quem usa a classe.