Python

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

14 min de leitura

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

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

<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):

&quot;&quot;&quot;Getter: retorna a idade&quot;&quot;&quot;

return self._idade

@idade.setter

def idade(self, valor):

&quot;&quot;&quot;Setter: validação antes de atribuir&quot;&quot;&quot;

if not isinstance(valor, int):

raise TypeError(&quot;Idade deve ser um inteiro&quot;)

if valor &lt; 0 or valor &gt; 150:

raise ValueError(&quot;Idade deve estar entre 0 e 150&quot;)

self._idade = valor

@idade.deleter

def idade(self):

&quot;&quot;&quot;Deleter: executado quando del pessoa.idade&quot;&quot;&quot;

print(&quot;Deletando a idade...&quot;)

del self._idade

Uso

p = Pessoa(&quot;João&quot;, 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 &lt;= 0:

raise ValueError(&quot;Raio deve ser positivo&quot;)

self._raio = valor

@property

def area(self):

&quot;&quot;&quot;Propriedade computada: nunca é armazenada&quot;&quot;&quot;

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:

&quot;&quot;&quot;Descritor para validar e converter temperaturas em Celsius&quot;&quot;&quot;

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(&quot;Temperatura deve ser numérica&quot;)

if valor &lt; -273.15:

raise ValueError(&quot;Temperatura não pode ser menor que -273.15°C&quot;)

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(&#039;_temp&#039;)

def __init__(self, nome):

self.nome = nome

self.temperatura = 20 # Usa __set__ do descritor

Uso

sala = Sala(&quot;Sala 1&quot;)

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&quot;{self.nome_attr} deve ser numérico&quot;)

if self.minimo is not None and valor &lt; self.minimo:

raise ValueError(f&quot;{self.nome_attr} não pode ser menor que {self.minimo}&quot;)

if self.maximo is not None and valor &gt; self.maximo:

raise ValueError(f&quot;{self.nome_attr} não pode ser maior que {self.maximo}&quot;)

obj.__dict__[self.nome_attr] = valor

class Produto:

preco = ValidadorNumerico(&#039;_preco&#039;, minimo=0)

quantidade = ValidadorNumerico(&#039;_quantidade&#039;, minimo=0, maximo=1000)

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

self.nome = nome

self.preco = preco

self.quantidade = quantidade

class Aluno:

nota = ValidadorNumerico(&#039;_nota&#039;, minimo=0, maximo=10)

def __init__(self, nome, nota):

self.nome = nome

self.nota = nota

Uso

p = Produto(&quot;Notebook&quot;, 2000, 5)

p.preco = 2500 # Valida

a = Aluno(&quot;Maria&quot;, 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:

&quot;&quot;&quot;Descritor que transforma métodos em funções com contexto&quot;&quot;&quot;

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&quot;Chamando {self.func.__name__} em {obj}&quot;)

return self.func(obj, args, *kwargs)

return wrapper

class Usuario:

def __init__(self, nome):

self.nome = nome

@Funcao

def saudar(self):

return f&quot;Olá, {self.nome}&quot;

Uso

u = Usuario(&quot;Ana&quot;)

print(u.saudar()) # Imprime &quot;Chamando saudar em &lt;Usuario object&gt;&quot; e depois &quot;Olá, Ana&quot;

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 &lt; 0:

raise ValueError(&quot;Saldo não pode ser negativo&quot;)

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&#039;_{self.tipo}&#039;)

def __set__(self, obj, valor):

if self.tipo == &#039;numero&#039; and not isinstance(valor, (int, float)):

raise TypeError(&quot;Deve ser número&quot;)

if self.tipo == &#039;texto&#039; and not isinstance(valor, str):

raise TypeError(&quot;Deve ser texto&quot;)

obj.__dict__[f&#039;_{self.tipo}&#039;] = valor

class Livro:

titulo = Validador(&#039;texto&#039;)

paginas = Validador(&#039;numero&#039;)

def __init__(self, titulo, paginas):

self.titulo = titulo

self.paginas = paginas

class Artigo:

titulo = Validador(&#039;texto&#039;)

visualizacoes = Validador(&#039;numero&#039;)

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(&quot;Direto:&quot;, timeit.timeit(lambda: d.valor, number=1000000))

print(&quot;Property:&quot;, timeit.timeit(lambda: p.valor, number=1000000))

print(&quot;Descritor:&quot;, 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:

&quot;&quot;&quot;

Exemplo de boas práticas com properties.

&quot;&quot;&quot;

def __init__(self, valor):

self._valor = valor

self._acessos = 0

@property

def valor(self):

&quot;&quot;&quot;

Retorna o valor armazenado.

Nota: Não tem side effects além de incrementar contador interno.

&quot;&quot;&quot;

self._acessos += 1

return self._valor

@valor.setter

def valor(self, novo):

&quot;&quot;&quot;Define um novo valor com validação.&quot;&quot;&quot;

if not isinstance(novo, (int, float)):

raise TypeError(&quot;Deve ser numérico&quot;)

self._valor = novo

@property

def acessos(self):

&quot;&quot;&quot;Retorna quantas vezes o valor foi acessado.&quot;&quot;&quot;

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Herança e Polimorfismo em Python: MRO e super() na Prática
Herança e Polimorfismo em Python: MRO e super() na Prática

Fundamentos de Herança em Python A herança é um dos pilares da Programação Or...

Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis
Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis

Entendendo Generators: O Que Realmente São Um generator em Python é uma funçã...

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado
Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profis...