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