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 Silva

Note 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 negativo

Observe 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

Exercícios

  1. Crie uma classe Produto com atributos privados _nome e _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)
    
  2. Adicione à classe Produto uma property preco_com_desconto que 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.9
    
  3. Crie uma classe Pessoa com atributos _nome, _idade. Implemente property idade com validação (0 a 150) e property maior_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 >= 18
    
  4. Implemente uma classe ContaCorrente com _saldo e _limite. Crie property saldo_disponivel que retorna saldo + limite (apenas leitura) e property limite com 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._limite
    
  5. Crie uma classe Circulo com atributo _raio. Implemente property raio com validação (positivo) e properties area e circunferencia (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