Python Admin

Como Usar Properties e Descritores em Python: @property e __get__ __set__ em Produção Já leu

O Que São Properties e Descritores? Properties e descritores são mecanismos fundamentais em Python para controlar o acesso a atributos de uma classe. Eles permitem implementar lógica customizada quando você quer ler, escrever ou deletar um atributo, sem que o usuário da classe perceba que há algo além de um acesso direto. A diferença fundamental é que properties são um caso especial de descritores, mais simples e direto para casos comuns. Descritores, por sua vez, são um protocolo mais geral que oferece controle total sobre a vinculação de atributos. Quando você precisa de comportamentos complexos, reutilizáveis e que funcionem em múltiplas classes, descritores são sua melhor escolha. Vamos começar com um problema real: imagine uma classe com um atributo . Você quer garantir que ninguém atribua uma idade negativa ou maior que 150. Sem properties, você precisaria sempre usar métodos como e , o que é verboso e quebra a sintaxe natural do Python. Properties com @property Sintaxe e Comportamento

O Que São Properties e Descritores?

Properties e descritores são mecanismos fundamentais em Python para controlar o acesso a atributos de uma classe. Eles permitem implementar lógica customizada quando você quer ler, escrever ou deletar um atributo, sem que o usuário da classe perceba que há algo além de um acesso direto.

A diferença fundamental é que properties são um caso especial de descritores, mais simples e direto para casos comuns. Descritores, por sua vez, são um protocolo mais geral que oferece controle total sobre a vinculação de atributos. Quando você precisa de comportamentos complexos, reutilizáveis e que funcionem em múltiplas classes, descritores são sua melhor escolha.

Vamos começar com um problema real: imagine uma classe Pessoa com um atributo idade. Você quer garantir que ninguém atribua uma idade negativa ou maior que 150. Sem properties, você precisaria sempre usar métodos como set_idade() e get_idade(), o que é verboso e quebra a sintaxe natural do Python.

Properties com @property

Sintaxe e Comportamento Básico

A forma mais simples e comum de controlar atributos em Python é usar a decorator @property. Ela transforma um método em um atributo que parece ser acessado normalmente, mas executa código customizado por trás.

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self._idade = idade  # Convenção: _ indica uso interno

    @property
    def idade(self):
        """Getter: retorna a idade"""
        return self._idade

    @idade.setter
    def idade(self, valor):
        """Setter: validação antes de atribuir"""
        if not isinstance(valor, int):
            raise TypeError("Idade deve ser um inteiro")
        if valor < 0 or valor > 150:
            raise ValueError("Idade deve estar entre 0 e 150")
        self._idade = valor

    @idade.deleter
    def idade(self):
        """Deleter: executado quando del pessoa.idade"""
        print("Deletando a idade...")
        del self._idade

# Uso
p = Pessoa("João", 25)
print(p.idade)  # 25 - acessa o getter
p.idade = 30    # Executa o setter com validação
p.idade = -5    # Lança ValueError

Neste exemplo, _idade é o atributo real (privado por convenção). O @property cria uma propriedade que valida os dados. Quando você faz p.idade = 30, Python executa o setter automaticamente. Isso mantém a interface limpa enquanto protege os dados internos.

Cases de Uso Comuns para Properties

Properties brilham quando você precisa adicionar lógica simples a atributos já existentes, ou quando quer um atributo computado. Um exemplo clássico é um atributo que nunca é armazenado, mas calculado sob demanda.

import math

class Circulo:
    def __init__(self, raio):
        self._raio = raio

    @property
    def raio(self):
        return self._raio

    @raio.setter
    def raio(self, valor):
        if valor <= 0:
            raise ValueError("Raio deve ser positivo")
        self._raio = valor

    @property
    def area(self):
        """Propriedade computada: nunca é armazenada"""
        return math.pi * self._raio ** 2

    @property
    def perimetro(self):
        return 2 * math.pi * self._raio

# Uso
c = Circulo(5)
print(c.area)      # 78.53981633974483
print(c.perimetro) # 31.41592653589793
c.raio = 10        # Executa validação
print(c.area)      # 314.1592653589793

Aqui, area e perimetro são properties que não armazenam nada — apenas calculam o valor na hora. Quando o raio muda, essas propriedades sempre refletem o novo cálculo. Isso é elegante e poupa memória.

Descritores: O Nível Avançado

Entendendo o Protocolo de Descritores

Descritores são a base interna de properties. Um descritor é qualquer classe que implementa pelo menos um dos métodos especiais: __get__, __set__ ou __delete__. Quando você acessa um atributo, Python procura por um descritor na classe antes de procurar na instância.

class Temperatura:
    """Descritor para validar e converter temperaturas em Celsius"""

    def __init__(self, nome_attr):
        self.nome_attr = nome_attr

    def __get__(self, obj, objtype=None):
        # Executado quando você acessa o atributo
        if obj is None:
            return self
        return obj.__dict__.get(self.nome_attr, 0)

    def __set__(self, obj, valor):
        # Executado quando você atribui um valor
        if not isinstance(valor, (int, float)):
            raise TypeError("Temperatura deve ser numérica")
        if valor < -273.15:
            raise ValueError("Temperatura não pode ser menor que -273.15°C")
        obj.__dict__[self.nome_attr] = valor

    def __delete__(self, obj):
        # Executado quando você deleta o atributo
        del obj.__dict__[self.nome_attr]

class Sala:
    # Descritores são definidos na classe, não na instância
    temperatura = Temperatura('_temp')

    def __init__(self, nome):
        self.nome = nome
        self.temperatura = 20  # Usa __set__ do descritor

# Uso
sala = Sala("Sala 1")
print(sala.temperatura)  # 20 - usa __get__
sala.temperatura = 25    # Valida e armazena
sala.temperatura = -300  # Lança ValueError

O fluxo é assim: quando você faz sala.temperatura = 25, Python não simplesmente armazena o valor. Ele encontra que temperatura é um descritor na classe Sala e chama seu método __set__. Os dados reais são armazenados no __dict__ da instância com a chave _temp.

Diferenças Críticas Entre Properties e Descritores

Properties são descritores internamente. @property é açúcar sintático que cria uma classe descritora automaticamente. Mas descritores puros oferecem mais flexibilidade: você pode reutilizar o mesmo descritor em múltiplas classes, adicionar comportamentos complexos e até trabalhar com herança de forma sofisticada.

# Descritor reutilizável em múltiplas classes
class ValidadorNumerico:
    def __init__(self, nome_attr, minimo=None, maximo=None):
        self.nome_attr = nome_attr
        self.minimo = minimo
        self.maximo = maximo

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.nome_attr)

    def __set__(self, obj, valor):
        if not isinstance(valor, (int, float)):
            raise TypeError(f"{self.nome_attr} deve ser numérico")
        if self.minimo is not None and valor < self.minimo:
            raise ValueError(f"{self.nome_attr} não pode ser menor que {self.minimo}")
        if self.maximo is not None and valor > self.maximo:
            raise ValueError(f"{self.nome_attr} não pode ser maior que {self.maximo}")
        obj.__dict__[self.nome_attr] = valor

class Produto:
    preco = ValidadorNumerico('_preco', minimo=0)
    quantidade = ValidadorNumerico('_quantidade', minimo=0, maximo=1000)

    def __init__(self, nome, preco, quantidade):
        self.nome = nome
        self.preco = preco
        self.quantidade = quantidade

class Aluno:
    nota = ValidadorNumerico('_nota', minimo=0, maximo=10)

    def __init__(self, nome, nota):
        self.nome = nome
        self.nota = nota

# Uso
p = Produto("Notebook", 2000, 5)
p.preco = 2500  # Valida
a = Aluno("Maria", 8.5)
a.nota = 11     # Lança ValueError

Aqui, o mesmo ValidadorNumerico funciona em Produto e Aluno. Com properties, você teria que reescrever a lógica em cada classe. Descritores são reutilizáveis.

Descritores com get Avançado

O __get__ é especialmente poderoso. Ele recebe três argumentos: self (o descritor), obj (a instância que acessou o atributo) e objtype (a classe). Quando obj é None, significa que você acessou o atributo pela classe, não pela instância.

class Funcao:
    """Descritor que transforma métodos em funções com contexto"""

    def __init__(self, func):
        self.func = func
        self.__doc__ = func.__doc__

    def __get__(self, obj, objtype=None):
        if obj is None:
            # Acessado pela classe
            return self.func
        # Acessado pela instância
        def wrapper(*args, **kwargs):
            print(f"Chamando {self.func.__name__} em {obj}")
            return self.func(obj, *args, **kwargs)
        return wrapper

class Usuario:
    def __init__(self, nome):
        self.nome = nome

    @Funcao
    def saudar(self):
        return f"Olá, {self.nome}"

# Uso
u = Usuario("Ana")
print(u.saudar())  # Imprime "Chamando saudar em <Usuario object>" e depois "Olá, Ana"
print(Usuario.saudar)  # Retorna a função original

Esse padrão é avançado, mas mostra que descritores podem interceptar não apenas armazenamento, mas também como valores são recuperados e modificados.

Comparação Prática e Quando Usar Cada Um

A escolha entre properties e descritores não é binária — é contextual. Para 90% dos casos, properties resolvem elegantemente. Descritores entram em cena quando você precisa de reutilização, comportamentos muito complexos ou aplicar a mesma lógica a múltiplas classes.

# Scenario 1: Você quer uma propriedade simples em uma classe única
class ContaBancaria:
    def __init__(self, saldo_inicial):
        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

# Scenario 2: Mesma lógica em múltiplas classes, múltiplas vezes
class Validador:
    def __init__(self, tipo):
        self.tipo = tipo

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(f'_{self.tipo}')

    def __set__(self, obj, valor):
        if self.tipo == 'numero' and not isinstance(valor, (int, float)):
            raise TypeError("Deve ser número")
        if self.tipo == 'texto' and not isinstance(valor, str):
            raise TypeError("Deve ser texto")
        obj.__dict__[f'_{self.tipo}'] = valor

class Livro:
    titulo = Validador('texto')
    paginas = Validador('numero')

    def __init__(self, titulo, paginas):
        self.titulo = titulo
        self.paginas = paginas

class Artigo:
    titulo = Validador('texto')
    visualizacoes = Validador('numero')

    def __init__(self, titulo, visualizacoes):
        self.titulo = titulo
        self.visualizacoes = visualizacoes

No primeiro cenário, @property é clara e suficiente. No segundo, um descritor reutilizável elimina duplicação.

Performance e Boas Práticas

Properties têm overhead mínimo — é apenas uma chamada de função. Descritores têm um pouco mais de overhead porque Python precisa procurar na cadeia MRO (Method Resolution Order). Em aplicações críticas em performance, você pode precisar fazer benchmarks, mas na maioria dos casos a diferença é negligenciável comparado ao ganho em clareza de código.

import timeit

# Test 1: Acesso direto
class Direto:
    def __init__(self):
        self.valor = 42

# Test 2: Com property
class ComProperty:
    def __init__(self):
        self._valor = 42

    @property
    def valor(self):
        return self._valor

# Test 3: Com descritor
class MinhaProperty:
    def __get__(self, obj, objtype=None):
        return 42

class ComDescritor:
    valor = MinhaProperty()

# Benchmark
d = Direto()
p = ComProperty()
c = ComDescritor()

print("Direto:", timeit.timeit(lambda: d.valor, number=1000000))
print("Property:", timeit.timeit(lambda: p.valor, number=1000000))
print("Descritor:", timeit.timeit(lambda: c.valor, number=1000000))

A diferença costuma ser de milissegundos em milhões de acessos. Use properties e descritores sem medo de performance — use-os porque melhoram seu código.

Boas Práticas

Primeira: sempre use _atributo (com underscore) para armazenar dados reais quando implementa properties ou descritores. Isso sinaliza que é um detalhe interno. Segunda: documente o que cada property/descritor faz. Terceira: evite side effects em getters — eles devem ser leitura pura. Quarta: em descritores, sempre implemente __get__ se você implementar __set__ ou __delete__.

class Exemplo:
    """
    Exemplo de boas práticas com properties.
    """

    def __init__(self, valor):
        self._valor = valor
        self._acessos = 0

    @property
    def valor(self):
        """
        Retorna o valor armazenado.
        Nota: Não tem side effects além de incrementar contador interno.
        """
        self._acessos += 1
        return self._valor

    @valor.setter
    def valor(self, novo):
        """Define um novo valor com validação."""
        if not isinstance(novo, (int, float)):
            raise TypeError("Deve ser numérico")
        self._valor = novo

    @property
    def acessos(self):
        """Retorna quantas vezes o valor foi acessado."""
        return self._acessos

e = Exemplo(10)
print(e.valor)  # Incrementa contador
print(e.acessos)  # 1

Conclusão

Você aprendeu que properties são a forma elegante e pythônica de controlar acesso a atributos em a maioria das situações, oferecendo uma sintaxe clara com @property, @getter, @setter e @deleter. Elas transformam métodos em atributos sem que o usuário da classe perceba.

Também compreendeu que descritores são o mecanismo fundamental por trás de properties, oferecendo controle total através dos métodos __get__, __set__ e __delete__. Quando você precisa reutilizar a mesma lógica de validação em múltiplas classes ou implementar comportamentos avançados, descritores são a ferramenta correta.

Por fim, saiba que a escolha entre um e outro depende do contexto: properties para casos simples em uma única classe, descritores para lógica complexa e reutilizável. Ambos têm performance excelente e devem ser usados sem receio — o código limpo que você ganha compensa qualquer micro-overhead.

Referências

  • https://docs.python.org/3/howto/descriptor.html — Documentação oficial de descritores em Python
  • https://docs.python.org/3/library/functions.html#property — Documentação oficial de @property
  • https://realpython.com/python-descriptors/ — Guia detalhado sobre descritores da Real Python
  • Fluent Python by Luciano Ramalho, Capítulo 20-21 sobre Descriptors e Properties — Livro referência
  • https://www.geeksforgeeks.org/python-property-decorator-property/ — Tutorial prático de properties

Artigos relacionados