Propriedades com @property, getter e setter
1. Introdução às Propriedades em Python
Em Python, o encapsulamento é um princípio fundamental da programação orientada a objetos que visa proteger os dados internos de uma classe. Tradicionalmente, linguagens como Java utilizam métodos get_ e set_ para controlar o acesso aos atributos. Python oferece uma abordagem mais elegante através do decorador @property.
Antes de mergulharmos nas propriedades, é importante entender as convenções de acesso em Python:
- Atributos públicos:
self.nome— acessíveis diretamente - Atributos protegidos:
self._nome— convenção indicando uso interno - Atributos privados:
self.__nome— name mangling para evitar acesso acidental
O @property permite transformar métodos em atributos, combinando a simplicidade de acesso direto com a segurança de validação.
2. O Decorador @property para Getters
O decorador @property transforma um método em um atributo somente leitura. Vamos ver um exemplo prático:
class Retangulo:
def __init__(self, largura, altura):
self._largura = largura
self._altura = altura
@property
def area(self):
return self._largura * self._altura
@property
def largura(self):
return self._largura
@property
def altura(self):
return self._altura
# Uso
r = Retangulo(5, 3)
print(r.area) # 15 - acessado como atributo, não como método
print(r.largura) # 5
Aqui, area é um atributo calculado que não precisa ser armazenado. Cada acesso recalcula o valor baseado nos atributos atuais.
3. Implementando Setters com @<propriedade>.setter
Para permitir modificação controlada, usamos o decorador @<propriedade>.setter:
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 len(valor.strip()) == 0:
raise ValueError("Nome deve ser uma string não vazia")
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 um inteiro entre 0 e 150")
self._idade = valor
# Testando validações
p = Pessoa("Maria", 30)
print(p.nome) # Maria
p.idade = 31 # Válido
# p.idade = -5 # Levanta ValueError
4. O Decorador @<propriedade>.deleter
O @<propriedade>.deleter permite controlar o que acontece quando usamos del em uma propriedade:
class CacheResultado:
def __init__(self):
self._cache = {}
self._dados_processados = None
@property
def resultado(self):
if self._dados_processados is None:
print("Calculando resultado...")
self._dados_processados = sum(self._cache.values())
return self._dados_processados
@resultado.deleter
def resultado(self):
print("Cache invalidado")
self._dados_processados = None
def adicionar_dado(self, chave, valor):
self._cache[chave] = valor
self._dados_processados = None # Invalida cache
# Uso
cache = CacheResultado()
cache.adicionar_dado("a", 10)
cache.adicionar_dado("b", 20)
print(cache.resultado) # Calcula e retorna 30
del cache.resultado # Invalida cache
5. Propriedades como Atributos Calculados e em Tempo Real
Propriedades são ideais para cálculos sob demanda, especialmente quando o valor pode mudar:
class ContaBancaria:
def __init__(self, saldo_inicial, taxa_juros=0.01):
self._saldo = saldo_inicial
self._taxa_juros = taxa_juros
self._ultimo_acesso = None
@property
def saldo(self):
"""Calcula saldo com juros desde o último acesso"""
if self._ultimo_acesso:
from datetime import datetime, timedelta
dias_desde_acesso = (datetime.now() - self._ultimo_acesso).days
if dias_desde_acesso > 0:
self._saldo *= (1 + self._taxa_juros) ** dias_desde_acesso
self._ultimo_acesso = datetime.now()
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("Saldo não pode ser negativo")
self._saldo = valor
self._ultimo_acesso = None
conta = ContaBancaria(1000, 0.05)
print(f"Saldo atual: R${conta.saldo:.2f}")
6. Propriedades com Herança e Polimorfismo
Propriedades podem ser sobrescritas em subclasses, mantendo o comportamento polimórfico:
class Forma:
@property
def area(self):
raise NotImplementedError("Subclasses devem implementar area")
@property
def descricao(self):
return f"Forma com área {self.area:.2f}"
class Quadrado(Forma):
def __init__(self, lado):
self._lado = lado
@property
def lado(self):
return self._lado
@lado.setter
def lado(self, valor):
if valor <= 0:
raise ValueError("Lado deve ser positivo")
self._lado = valor
@property
def area(self):
return self._lado ** 2
class Circulo(Forma):
def __init__(self, raio):
self._raio = raio
@property
def raio(self):
return self._raio
@property
def area(self):
import math
return math.pi * self._raio ** 2
# Polimorfismo em ação
formas = [Quadrado(4), Circulo(3)]
for forma in formas:
print(forma.descricao)
7. Boas Práticas e Armadilhas Comuns
Quando usar @property:
- Para atributos calculados que dependem de outros atributos
- Quando é necessário validação ou transformação ao acessar/modificar
- Para manter compatibilidade com código existente ao adicionar lógica
Armadilhas a evitar:
# EVITE: Getter com efeitos colaterais
class Ruim:
@property
def dados(self):
self._contador += 1 # Efeito colateral em getter
return self._dados
# EVITE: Propriedade que levanta exceções inesperadas
class Perigoso:
@property
def valor(self):
if not hasattr(self, '_valor'):
raise AttributeError("Valor não inicializado") # Melhor usar None
return self._valor
# PREFIRA: Simplicidade quando não há lógica
class Bom:
def __init__(self):
self.valor = 10 # Atributo simples é suficiente
8. Comparação com Outras Abordagens
@property vs __getattr__/__setattr__:
# Usando __getattr__ (mais complexo e propenso a erros)
class UsandoGetAttr:
def __init__(self):
self._dados = {}
def __getattr__(self, nome):
if nome.startswith('_'):
raise AttributeError(nome)
return self._dados.get(nome)
def __setattr__(self, nome, valor):
if nome.startswith('_'):
super().__setattr__(nome, valor)
else:
self._dados[nome] = valor
# Usando @property (mais explícito e seguro)
class UsandoProperty:
def __init__(self):
self._dados = {}
@property
def nome(self):
return self._dados.get('nome')
@nome.setter
def nome(self, valor):
self._dados['nome'] = valor
@property vs dataclasses com validadores:
from dataclasses import dataclass, field
# dataclass sem validação
@dataclass
class PessoaDC:
nome: str
idade: int
# Com validação via __post_init__
@dataclass
class PessoaDCValidada:
nome: str
idade: int
def __post_init__(self):
if not isinstance(self.nome, str) or len(self.nome.strip()) == 0:
raise ValueError("Nome inválido")
if self.idade < 0 or self.idade > 150:
raise ValueError("Idade inválida")
# @property oferece mais controle e validação em tempo real
class PessoaProperty:
def __init__(self, nome, idade):
self._nome = nome
self._idade = idade
@property
def nome(self):
return self._nome
@nome.setter
def nome(self, valor):
# Validação e transformação em tempo real
valor = valor.strip().title()
if not valor:
raise ValueError("Nome não pode ser vazio")
self._nome = valor
O decorador @property é a abordagem mais pythonica para controle de acesso a atributos, oferecendo um equilíbrio perfeito entre simplicidade e poder de validação.
Referências
- Documentação Oficial: Property — Documentação completa da função property() e seus decoradores
- Real Python: Python @property Decorator — Tutorial abrangente com exemplos práticos do uso de @property
- GeeksforGeeks: @property Decorator in Python — Guia detalhado com exemplos de getters, setters e deleters
- Programiz: Python @property — Tutorial interativo explicando o conceito de propriedades em Python
- Python Docs: Descriptor HowTo Guide — Guia avançado sobre descritores, incluindo a implementação subjacente do @property
- Stack Overflow: Understanding @property in Python — Discussão técnica sobre o funcionamento interno do decorador @property