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.

Você vai aprender
  • A criar um atributo calculado com @property: bia.media, sem parênteses.
  • Por que um @property sozinho é só de leitura, e a mensagem has no setter.
  • A escrever o setter (@preco.setter), que confere o valor e levanta ValueError quando 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 RecursionError do self.preco dentro do próprio property, os parênteses a mais, o setter com outro nome e o @property esquecido.

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 escreveO Python chamaNome em inglês
cafe.preco (ler)O método marcado com @propertygetter, "pegador"
cafe.preco = 21.00 (mudar)O método marcado com @preco.settersetter, "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étodo media numa 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 com return.
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 o media(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 em self._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". O preco do @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, o valor que está do lado direito do =. Em cafe.preco = 21.00, o valor é 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__ escreve self.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.9 e 21.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 atributo preco no objeto: o preco é a propriedade, que mora na classe.
  • File "/home/ana/setter.py", line 22, in <module>: o erro começou na linha 22, a do cafe.preco = -5.
  • File "/home/ana/setter.py", line 13, in preco: e estourou na linha 13, dentro do setter preco, no raise.
  • 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.

Conferindo o tipo também

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.

FormaQuando usar
Atributo comum: self.preco = precoComece por aqui. Se não há regra nenhuma, não precisa de property
@property sozinhoValor calculado (media) ou só de leitura (saldo)
@property + @preco.setterQuando 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

O property chamando a si mesmo: 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.

Parênteses a mais: '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.

Esquecer o @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).

O setter com outro nome: 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

EscritaO 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._precoOnde 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.deleterExiste também: roda em del obj.preco. Raramente usado
SintomaCausa provável
AttributeError: property 'media' of 'Aluno' object has no setterEscreveu numa propriedade só de leitura (de propósito ou não), ou o setter tem outro nome
RecursionError: maximum recursion depth exceededO getter ou o setter usa self.preco no lugar de self._preco
TypeError: 'float' object is not callableParê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 erroO __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

  • @property transforma 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 cada obj.preco = valor; ele confere e levanta ValueError (ou TypeError) quando o valor não serve.
  • O valor de verdade mora num nome diferente, com sublinhado: self._preco. Usar self.preco dentro da própria propriedade dá RecursionError.
  • No __init__, escreva self.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.