Properties
Esta aula aborda o uso de properties em Python, incluindo a criação de getters, setters e atributos computados com o decorador @property. Serão apresentados exemplos práticos de validação de dados e boas práticas para encapsulamento.
Properties em Python permitem controlar o acesso a atributos de uma classe de forma elegante, combinando a simplicidade da sintaxe de atributos com a flexibilidade de métodos. Com o decorador @property, podemos definir métodos que são acessados como atributos, possibilitando validação, lazy evaluation e atributos computados.
Nesta aula, exploraremos como criar getters e setters usando properties, como validar dados durante a atribuição e como implementar atributos que são calculados dinamicamente.
@property
O decorador @property transforma um método em um atributo somente leitura. Isso é útil quando queremos encapsular a lógica de obtenção de um valor, mas manter a interface de acesso simples.
Por exemplo, em uma classe Pessoa, podemos ter um atributo nome e uma property nome_completo que combina nome e sobrenome.
class Pessoa:
def __init__(self, nome, sobrenome):
self._nome = nome
self._sobrenome = sobrenome
@property
def nome_completo(self):
return f"{self._nome} {self._sobrenome}"
p = Pessoa("João", "Silva")
print(p.nome_completo) # João SilvaNote que nome_completo é acessado como um atributo, sem parênteses. Internamente, o método é chamado. Isso permite que futuras alterações na implementação não quebrem o código cliente.
Getters e setters
Além do getter (com @property), podemos definir um setter usando o decorador @nome_da_property.setter. Isso permite controlar a atribuição de valores, adicionando validação ou transformações.
Exemplo com um atributo _temperatura em Celsius, mas com property que aceita tanto Celsius quanto Fahrenheit.
class Termometro:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, valor):
if valor < -273.15:
raise ValueError("Temperatura abaixo do zero absoluto")
self._celsius = valor
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, valor):
self.celsius = (valor - 32) * 5/9
t = Termometro(25)
t.celsius = 30 # usa setter
t.fahrenheit = 100 # converte para Celsius via setter
print(t.celsius) # 37.777...O setter de fahrenheit converte o valor e atribui ao atributo _celsius através do setter de celsius, que por sua vez valida. Isso mostra como properties podem ser encadeadas.
Validação
Properties são ideais para validação de dados. Podemos verificar tipos, limites ou outras regras de negócio dentro do setter.
Exemplo: uma classe ContaBancaria com saldo que não pode ser negativo.
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 not isinstance(valor, (int, float)):
raise TypeError("Saldo deve ser numérico")
if valor < 0:
raise ValueError("Saldo não pode ser negativo")
self._saldo = valor
def depositar(self, valor):
self.saldo += valor # usa setter
def sacar(self, valor):
if valor > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= valor # usa setter
c = ContaBancaria("Maria", 1000)
c.depositar(500)
print(c.saldo) # 1500
c.saldo = -100 # ValueError: Saldo não pode ser negativoObserve que ao usar self.saldo += valor, o Python chama o getter para ler o valor atual e depois o setter para atribuir o novo valor. Isso garante que a validação seja aplicada em todas as modificações.
Atributos computados
Atributos computados são aqueles cujo valor é derivado de outros atributos. Properties permitem que esses atributos sejam acessados como se fossem armazenados, mas são calculados sob demanda.
Exemplo: uma classe Retangulo com area e perimetro computados.
class Retangulo:
def __init__(self, largura, altura):
self._largura = largura
self._altura = altura
@property
def largura(self):
return self._largura
@largura.setter
def largura(self, valor):
if valor <= 0:
raise ValueError("Largura deve ser positiva")
self._largura = valor
@property
def altura(self):
return self._altura
@altura.setter
def altura(self, valor):
if valor <= 0:
raise ValueError("Altura deve ser positiva")
self._altura = valor
@property
def area(self):
return self._largura * self._altura
@property
def perimetro(self):
return 2 * (self._largura + self._altura)
r = Retangulo(10, 5)
print(r.area) # 50
print(r.perimetro) # 30
r.largura = 20
print(r.area) # 100 (recalculado)Atributos computados são readonly (não possuem setter) porque não faz sentido atribuir um valor diretamente a eles. Eles sempre refletem o estado atual dos atributos base.
Boas práticas
Use properties para:
- Encapsular lógica de acesso a atributos.
- Adicionar validação sem expor o atributo privado.
- Calcular valores dinamicamente.
- Manter a interface pública estável enquanto a implementação interna muda.
Evite properties para operações pesadas (como acesso a banco de dados) sem cache, pois o método é chamado toda vez que o atributo é acessado. Nesses casos, considere usar functools.cached_property (Python 3.8+).
Referências
- Documentação oficial: property
- Python Descriptor HowTo
- Real Python: Python's property()
- GeeksforGeeks: @property decorator
- Stack Overflow: How does @property work?
Exercícios
Crie uma classe
Produtocom atributos privados_nomee_preco. Implemente properties para ambos, com validação: nome deve ser string não vazia, preço deve ser float positivo.✓ Resposta:class Produto: def __init__(self, nome, preco): self.nome = nome # usa setter self.preco = preco @property def nome(self): return self._nome @nome.setter def nome(self, valor): if not isinstance(valor, str) or not valor.strip(): raise ValueError("Nome deve ser uma string não vazia") self._nome = valor.strip() @property def preco(self): return self._preco @preco.setter def preco(self, valor): if not isinstance(valor, (int, float)) or valor <= 0: raise ValueError("Preço deve ser um número positivo") self._preco = float(valor)Adicione à classe
Produtouma propertypreco_com_descontoque retorna o preço com 10% de desconto (apenas leitura).✓ Resposta:class Produto: # ... (código anterior) ... @property def preco_com_desconto(self): return self._preco * 0.9Crie uma classe
Pessoacom atributos_nome,_idade. Implemente propertyidadecom validação (0 a 150) e propertymaior_de_idade(bool, computado).✓ Resposta:class Pessoa: def __init__(self, nome, idade): self.nome = nome self.idade = idade @property def nome(self): return self._nome @nome.setter def nome(self, valor): if not isinstance(valor, str) or not valor.strip(): raise ValueError("Nome inválido") self._nome = valor.strip() @property def idade(self): return self._idade @idade.setter def idade(self, valor): if not isinstance(valor, int) or valor < 0 or valor > 150: raise ValueError("Idade deve ser inteiro entre 0 e 150") self._idade = valor @property def maior_de_idade(self): return self._idade >= 18Implemente uma classe
ContaCorrentecom_saldoe_limite. Crie propertysaldo_disponivelque retorna saldo + limite (apenas leitura) e propertylimitecom validação (não negativo).✓ Resposta:class ContaCorrente: def __init__(self, saldo, limite): self._saldo = saldo self.limite = limite # usa setter @property def saldo(self): return self._saldo @property def limite(self): return self._limite @limite.setter def limite(self, valor): if not isinstance(valor, (int, float)) or valor < 0: raise ValueError("Limite não pode ser negativo") self._limite = float(valor) @property def saldo_disponivel(self): return self._saldo + self._limiteCrie uma classe
Circulocom atributo_raio. Implemente propertyraiocom validação (positivo) e propertiesareaecircunferencia(computados).✓ Resposta:import math class Circulo: def __init__(self, raio): self.raio = raio # usa setter @property def raio(self): return self._raio @raio.setter def raio(self, valor): if not isinstance(valor, (int, float)) or valor <= 0: raise ValueError("Raio deve ser positivo") self._raio = float(valor) @property def area(self): return math.pi * self._raio ** 2 @property def circunferencia(self): return 2 * math.pi * self._raio