Properties são um recurso poderoso do Python que permite que métodos sejam acessados como se fossem atributos simples. Elas são frequentemente usadas para implementar getters, setters e deleters de forma controlada, mantendo a interface limpa e permitindo adicionar lógica extra (como validação) sem alterar a forma como a classe é usada externamente.

Nesta aula, exploraremos o decorador @property, que transforma um método em um atributo somente leitura, e também como criar setters e deleters usando @nome.setter e @nome.deleter. Veremos como validar dados, calcular atributos dinamicamente e boas práticas para usar properties em suas classes.

@property

O decorador @property é a forma mais simples de criar uma property. Ele permite que um método seja acessado como um atributo, sem parênteses. Isso é útil quando você quer expor um valor calculado ou controlar o acesso a um atributo privado.

Por exemplo, suponha uma classe Pessoa com um atributo _nome (privado). Podemos criar uma property nome que retorna o valor de _nome. Assim, externamente, usamos pessoa.nome em vez de pessoa._nome, e ainda podemos modificar a implementação interna sem afetar o código cliente.

class Pessoa:
    def __init__(self, nome):
        self._nome = nome

    @property
    def nome(self):
        return self._nome

p = Pessoa("João")
print(p.nome)  # João
# p.nome = "Maria"  # AttributeError: can't set attribute

Note que, sem um setter, a property é somente leitura. Tentar atribuir um valor causa um AttributeError.

Getters e setters

Para permitir a modificação controlada de um atributo, usamos o decorador @nome.setter. O método setter deve ter o mesmo nome da property e receber o valor a ser atribuído. Isso nos permite executar lógica adicional, como validação, antes de armazenar o valor.

O padrão é: criar um atributo privado (com underline), e expor properties para get e set. Exemplo:

class Pessoa:
    def __init__(self, nome):
        self._nome = nome

    @property
    def nome(self):
        return self._nome

    @nome.setter
    def nome(self, valor):
        if not isinstance(valor, str):
            raise TypeError("Nome deve ser uma string")
        self._nome = valor

p = Pessoa("João")
p.nome = "Maria"  # ok
print(p.nome)      # Maria
# p.nome = 123     # TypeError

Também podemos definir um deleter com @nome.deleter, que é chamado ao usar del objeto.nome. É menos comum, mas útil para liberar recursos.

Validação

Properties são ideais para adicionar validação de dados. No setter, podemos verificar tipos, intervalos, formatos, etc., e levantar exceções apropriadas se os dados forem inválidos. Isso mantém a classe robusta e evita estados inconsistentes.

Exemplo: uma classe ContaBancaria com saldo que não pode ser negativo.

class ContaBancaria:
    def __init__(self, saldo_inicial=0):
        self._saldo = saldo_inicial

    @property
    def saldo(self):
        return self._saldo

    @saldo.setter
    def saldo(self, valor):
        if valor < 0:
            raise ValueError("Saldo não pode ser negativo")
        self._saldo = valor

    def depositar(self, quantia):
        self.saldo += quantia  # usa o setter para validar

    def sacar(self, quantia):
        if quantia > self.saldo:
            raise ValueError("Saldo insuficiente")
        self.saldo -= quantia  # usa o setter

c = ContaBancaria(100)
c.depositar(50)
print(c.saldo)  # 150
c.sacar(200)    # ValueError: Saldo insuficiente

Note que usamos o setter internamente nos métodos, garantindo que a validação seja sempre aplicada.

Atributos computados

Properties também são usadas para criar atributos que são calculados dinamicamente a partir de outros dados. Eles não precisam de um atributo privado correspondente; o getter simplesmente computa e retorna o valor.

Exemplo: uma classe Retangulo com largura e altura, e uma property area que retorna o produto.

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

r = Retangulo(10, 5)
print(r.area)  # 50
r.largura = 20
print(r.area)  # 100

Atributos computados são somente leitura (não têm setter) porque não faz sentido atribuir um valor que é derivado de outros. Eles são recalculados toda vez que são acessados, o que pode ser um problema de desempenho se o cálculo for caro; nesse caso, pode-se usar cache (por exemplo, armazenar o resultado e invalidar quando os dependentes mudarem).

Boas práticas e observações finais

  • Use properties para expor atributos privados com controle, evitando getters e setters explícitos como em Java.
  • Não abuse: se um atributo não precisa de validação ou lógica extra, use um atributo público simples. Properties devem ser adicionadas quando houver necessidade.
  • Prefira @property a métodos como get_nome() e set_nome() — é mais Pythonico.
  • Lembre-se de que o nome da property e do atributo privado podem ser diferentes, mas é comum usar o mesmo nome com underline para o privado.
  • Para properties somente leitura, não defina setter; para leitura e escrita, defina ambos; para deleção, defina o deleter se necessário.

Referências

Exercícios

  1. Crie uma classe Termometro que armazena a temperatura em Celsius internamente, mas expõe uma property fahrenheit que retorna o valor convertido para Fahrenheit (F = C * 9/5 + 32). A property deve ser somente leitura.

    ✓ Resposta:
    class Termometro:
        def __init__(self, celsius):
            self._celsius = celsius
    
        @property
        def fahrenheit(self):
            return self._celsius * 9/5 + 32
    
  2. Implemente uma classe Produto com atributos privados _nome e _preco. Crie properties com getters e setters, onde o setter de preço valida que o valor é um número positivo. O setter de nome deve garantir que o nome não seja vazio.

    ✓ Resposta:
    class Produto:
        def __init__(self, nome, preco):
            self._nome = nome
            self._preco = preco
    
        @property
        def nome(self):
            return self._nome
    
        @nome.setter
        def nome(self, valor):
            if not valor.strip():
                raise ValueError("Nome não pode ser vazio")
            self._nome = valor
    
        @property
        def preco(self):
            return self._preco
    
        @preco.setter
        def preco(self, valor):
            if valor <= 0:
                raise ValueError("Preço deve ser positivo")
            self._preco = valor
    
  3. Na classe Retangulo do exemplo, adicione uma property perimetro que retorna o perímetro (2 * (largura + altura)).

    ✓ Resposta:
    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)
    
  4. Crie uma classe Pessoa com um atributo privado _idade. A property idade deve ter getter e setter, e o setter deve validar que a idade está entre 0 e 150. Além disso, adicione um deleter que define a idade como 0 (apenas para exemplo).

    ✓ Resposta:
    class Pessoa:
        def __init__(self, idade):
            self._idade = idade
    
        @property
        def idade(self):
            return self._idade
    
        @idade.setter
        def idade(self, valor):
            if not (0 <= valor <= 150):
                raise ValueError("Idade deve estar entre 0 e 150")
            self._idade = valor
    
        @idade.deleter
        def idade(self):
            self._idade = 0
    
  5. Crie uma classe Livro com atributos privados _titulo, _autor e _ano. Crie uma property descricao que retorna uma string formatada: "'Titulo' por Autor (Ano)". A property deve ser somente leitura.

    ✓ Resposta:
    class Livro:
        def __init__(self, titulo, autor, ano):
            self._titulo = titulo
            self._autor = autor
            self._ano = ano
    
        @property
        def descricao(self):
            return f"'{self._titulo}' por {self._autor} ({self._ano})"