<h2>O Que São Properties e Descritores?</h2>
<p>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.</p>
<p>A diferença fundamental é que <strong>properties</strong> são um caso especial de descritores, mais simples e direto para casos comuns. <strong>Descritores</strong>, 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.</p>
<p>Vamos começar com um problema real: imagine uma classe <code>Pessoa</code> com um atributo <code>idade</code>. Você quer garantir que ninguém atribua uma idade negativa ou maior que 150. Sem properties, você precisaria sempre usar métodos como <code>set_idade()</code> e <code>get_idade()</code>, o que é verboso e quebra a sintaxe natural do Python.</p>
<h2>Properties com @property</h2>
<h3>Sintaxe e Comportamento Básico</h3>
<p>A forma mais simples e comum de controlar atributos em Python é usar a decorator <code>@property</code>. Ela transforma um método em um atributo que parece ser acessado normalmente, mas executa código customizado por trás.</p>
<pre><code class="language-python">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</code></pre>
<p>Neste exemplo, <code>_idade</code> é o atributo real (privado por convenção). O <code>@property</code> cria uma propriedade que valida os dados. Quando você faz <code>p.idade = 30</code>, Python executa o setter automaticamente. Isso mantém a interface limpa enquanto protege os dados internos.</p>
<h3>Cases de Uso Comuns para Properties</h3>
<p>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.</p>
<pre><code class="language-python">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</code></pre>
<p>Aqui, <code>area</code> e <code>perimetro</code> 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.</p>
<h2>Descritores: O Nível Avançado</h2>
<h3>Entendendo o Protocolo de Descritores</h3>
<p>Descritores são a base interna de properties. Um descritor é qualquer classe que implementa pelo menos um dos métodos especiais: <code>__get__</code>, <code>__set__</code> ou <code>__delete__</code>. Quando você acessa um atributo, Python procura por um descritor na classe antes de procurar na instância.</p>
<pre><code class="language-python">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</code></pre>
<p>O fluxo é assim: quando você faz <code>sala.temperatura = 25</code>, Python não simplesmente armazena o valor. Ele encontra que <code>temperatura</code> é um descritor na classe <code>Sala</code> e chama seu método <code>__set__</code>. Os dados reais são armazenados no <code>__dict__</code> da instância com a chave <code>_temp</code>.</p>
<h3>Diferenças Críticas Entre Properties e Descritores</h3>
<p>Properties são descritores internamente. <code>@property</code> é 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.</p>
<pre><code class="language-python"># 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</code></pre>
<p>Aqui, o mesmo <code>ValidadorNumerico</code> funciona em <code>Produto</code> e <code>Aluno</code>. Com properties, você teria que reescrever a lógica em cada classe. Descritores são reutilizáveis.</p>
<h3>Descritores com __get__ Avançado</h3>
<p>O <code>__get__</code> é especialmente poderoso. Ele recebe três argumentos: <code>self</code> (o descritor), <code>obj</code> (a instância que acessou o atributo) e <code>objtype</code> (a classe). Quando <code>obj</code> é <code>None</code>, significa que você acessou o atributo pela classe, não pela instância.</p>
<pre><code class="language-python">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</code></pre>
<p>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.</p>
<h2>Comparação Prática e Quando Usar Cada Um</h2>
<p>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.</p>
<pre><code class="language-python"># 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</code></pre>
<p>No primeiro cenário, <code>@property</code> é clara e suficiente. No segundo, um descritor reutilizável elimina duplicação.</p>
<h2>Performance e Boas Práticas</h2>
<p>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.</p>
<pre><code class="language-python">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))</code></pre>
<p>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.</p>
<h3>Boas Práticas</h3>
<p>Primeira: sempre use <code>_atributo</code> (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 <code>__get__</code> se você implementar <code>__set__</code> ou <code>__delete__</code>.</p>
<pre><code class="language-python">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</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>properties são a forma elegante e pythônica de controlar acesso a atributos em a maioria das situações</strong>, oferecendo uma sintaxe clara com <code>@property</code>, <code>@getter</code>, <code>@setter</code> e <code>@deleter</code>. Elas transformam métodos em atributos sem que o usuário da classe perceba.</p>
<p>Também compreendeu que <strong>descritores são o mecanismo fundamental por trás de properties</strong>, oferecendo controle total através dos métodos <code>__get__</code>, <code>__set__</code> e <code>__delete__</code>. 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.</p>
<p>Por fim, saiba que <strong>a escolha entre um e outro depende do contexto</strong>: 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.</p>
<h2>Referências</h2>
<ul>
<li>https://docs.python.org/3/howto/descriptor.html — Documentação oficial de descritores em Python</li>
<li>https://docs.python.org/3/library/functions.html#property — Documentação oficial de @property</li>
<li>https://realpython.com/python-descriptors/ — Guia detalhado sobre descritores da Real Python</li>
<li>Fluent Python by Luciano Ramalho, Capítulo 20-21 sobre Descriptors e Properties — Livro referência</li>
<li>https://www.geeksforgeeks.org/python-property-decorator-property/ — Tutorial prático de properties</li>
</ul>
<p><!-- FIM --></p>