Python @property: Getters e Setters
Aprenda o decorador @property do Python: crie getters, setters, deleters e atributos calculados com sintaxe limpa e controle total de validação.
O decorador @property é o mecanismo embutido do Python para transformar um método em um atributo gerenciado. Em vez de escrever métodos get_x() e set_x() como em outras linguagens, você escreve um acesso a atributo de aparência normal (obj.x) mantendo controle total sobre o que acontece quando esse atributo é lido, escrito ou excluído.
Este capítulo aborda:
- Por que properties existem e quando usá-las
- Criando uma property somente leitura com
@property - Adicionando um setter com
@<name>.setter - Adicionando um deleter com
@<name>.deleter - Properties calculadas (derivadas)
- Atualizando um atributo simples para uma property sem quebrar o código existente
- A função embutida
property()— o mecanismo subjacente ao decorador - Como as properties funcionam como descritores (uma breve visão interna)
- Armadilhas comuns
Antes de ler, certifique-se de estar familiarizado com classes e objetos em Python. Properties são uma ferramenta fundamental para o encapsulamento em Python. Para métodos de classe e estáticos, consulte @staticmethod e @classmethod.
Por Que as Properties Existem
Considere uma classe que armazena uma temperatura em Celsius. Uma implementação ingênua expõe o valor interno diretamente:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t = Temperature(25)
t.celsius = -5000 # nothing stops this — physically impossibleO problema: nada impede que os chamadores definam uma temperatura abaixo do zero absoluto (−273,15 °C). Você poderia adicionar um método set_celsius() com validação, mas os chamadores precisariam alterar seu código de t.celsius = 100 para t.set_celsius(100) — uma mudança de API incompatível.
@property resolve isso de forma elegante. Você mantém a sintaxe t.celsius = 100 enquanto adiciona uma camada de controle nos bastidores.
Getter Básico: Acesso Somente Leitura
O uso mais simples de @property é um atributo somente leitura respaldado por uma variável privada:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # store in a private attribute
@property
def celsius(self):
return self._celsiusO decorador @property faz com que celsius pareça um atributo simples para o chamador:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses; Python calls the getter automaticallyComo não há setter, tentar atribuir um valor levanta um erro:
t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setterEsta é a forma correta de modelar um valor que deve ser definido apenas no momento da construção ou por meio de métodos específicos.
Adicionando um Setter com Validação
Decore um segundo método com @<property_name>.setter para lidar com escritas:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = valueAgora tanto a leitura quanto a escrita funcionam com a sintaxe de atributo simples:
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 100
print(t.celsius) # 100
t.celsius = -300 # ValueError: Temperature below absolute zeroRegra fundamental: o setter e o getter devem ter o mesmo nome (celsius nos dois casos). O decorador @celsius.setter vincula o novo método ao objeto property celsius já existente.
Properties Calculadas
Uma property não precisa corresponder a um atributo armazenado. Ela pode calcular um valor dinamicamente a partir de outros dados:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32fahrenheit não possui variável de apoio — ela deriva seu valor de _celsius sempre que é lida:
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.celsius = 100
print(t.fahrenheit) # 212.0Como não há @fahrenheit.setter, tentar escrever t.fahrenheit = 100 levanta um AttributeError. Properties calculadas são naturalmente somente leitura, a menos que você adicione explicitamente um setter.
Um exemplo real de property calculada
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('Width must be positive')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('Height must be positive')
self._height = value
@property
def area(self):
return self._width * self._height # computed; no setter
@property
def perimeter(self):
return 2 * (self._width + self._height) # computed; no setter
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
r.width = 10
print(r.area) # 50
r.width = -1 # ValueError: Width must be positiveAdicionando um Deleter
O decorador @<property_name>.deleter permite executar código quando o chamador usa del obj.attr:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@celsius.deleter
def celsius(self):
print('Deleting celsius')
del self._celsius
t = Temperature(25)
del t.celsius # Deleting celsius
print(t.celsius) # AttributeError: 'Temperature' object has no attribute '_celsius'Deleters são usados com menos frequência do que getters e setters. Eles são úteis quando:
- Remover um valor em cache para forçar o recálculo no próximo acesso.
- Liberar explicitamente recursos vinculados a um atributo.
- Garantir que, uma vez excluído, um valor não possa ser relido sem reatribuição.
Atualizando um Atributo Simples para uma Property
Um dos maiores benefícios práticos de @property é que você pode começar com um atributo público simples e adicionar validação posteriormente sem alterar nenhum código chamador. Isso é às vezes chamado de princípio de acesso uniforme.
# Version 1 — plain attribute, no validation
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # works, but nothing stops c.radius = -1Mais tarde você precisa de validação. Com @property você pode adicioná-la sem tocar nos chamadores:
# Version 2 — property with validation; public interface unchanged
import math
class Circle:
def __init__(self, radius):
self.radius = radius # this now calls the setter
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
@property
def area(self):
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(f'{c.area:.4f}') # 78.5398
c.radius = 10
print(c.radius) # 10
c.radius = -1 # ValueError: Radius cannot be negativeTodo código existente que lê ou escreve c.radius continua funcionando sem modificação.
A Função Embutida property()
@property é açúcar sintático para a função embutida property(). Estas duas definições são equivalentes:
# --- decorator style (recommended) ---
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value# --- property() style (explicit) ---
class Person:
def __init__(self, age):
self._age = age
def _get_age(self):
return self._age
def _set_age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value
def _del_age(self):
del self._age
age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')property(fget, fset, fdel, doc) aceita até quatro argumentos: uma função getter, uma função setter, uma função deleter e uma docstring. Qualquer um deles pode ser None.
p = Person(30)
print(p.age) # 30
p.age = 31
print(p.age) # 31
print(Person.age.__doc__) # The person's age in yearsA forma com decorador é mais limpa e é a recomendação padrão. A chamada explícita a property() é útil quando você quer passar a docstring sem um bloco de decorador multilinha, ou quando as funções de acesso já existem com outro nome.
Como as Properties Funcionam: Uma Breve Visão sobre Descritores
Internamente, property é um descritor — um objeto que define __get__, __set__ e __delete__ na classe. Quando Python busca obj.attr, ele verifica se o atributo na classe é um descritor e, em caso afirmativo, chama seu __get__ em vez de retornar o valor diretamente.
Você pode ver isso inspecionando o objeto property na classe:
class Square:
def __init__(self, side):
self._side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if value < 0:
raise ValueError('Side must be non-negative')
self._side = value
print(type(Square.side)) # <class 'property'>
print(Square.side.fget) # <function Square.side at 0x...>
print(Square.side.fset) # <function Square.side at 0x...>
print(Square.side.fdel) # NoneÉ por isso que ler Square.side retorna o próprio objeto property (descritor acessado na classe), enquanto ler s.side em uma instância aciona __get__ e retorna o inteiro. O protocolo de descritor é o mesmo mecanismo usado por classmethod, staticmethod e as próprias funções. Para um aprofundamento, consulte métodos mágicos do Python.
Armadilhas Comuns
Recursão infinita: esquecer o underscore
Um erro muito comum é usar o mesmo nome tanto para a property quanto para o atributo de apoio:
class Bad:
@property
def value(self):
return self.value # RecursionError! This calls the getter again
@value.setter
def value(self, v):
self.value = v # RecursionError! This calls the setter againSempre armazene o valor de apoio em um nome diferente, por convenção prefixado com um underscore:
class Good:
@property
def value(self):
return self._value # reads the private attribute
@value.setter
def value(self, v):
self._value = v # writes the private attributeSetter definido antes do getter
O decorador setter @celsius.setter referencia o objeto property celsius, que deve existir primeiro. Sempre defina o getter (@property) antes do setter e do deleter no corpo da classe.
__init__ chama o setter automaticamente
Quando você escreve self.radius = radius dentro de __init__, Python chama o setter (se existir). Isso geralmente é o comportamento desejado — a validação também é executada no momento da construção. Mas isso significa que seu setter deve lidar com a atribuição inicial de forma adequada:
class Circle:
def __init__(self, radius):
self.radius = radius # triggers the setter — validation applies here too
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
Circle(-1) # ValueError: Radius cannot be negativeProperties são de nível de classe, não de instância
Você não pode adicionar uma property a uma única instância da forma como pode com atributos regulares. Properties são definidas na classe e se aplicam a todas as instâncias. Se você precisar de personalização de atributos por instância, consulte dataclasses do Python ou use uma abordagem baseada em __slots__.
Referência Rápida
| Sintaxe | O que faz |
|---|---|
@property | Define o getter; o atributo se torna somente leitura até que um setter seja adicionado |
@<name>.setter | Define o setter; o atributo se torna legível e gravável |
@<name>.deleter | Define o deleter; del obj.attr aciona este método |
property(fget, fset, fdel, doc) | Equivalente embutido sem a sintaxe de decorador |
ClassName.prop.fget | A função getter subjacente |
ClassName.prop.fset | A função setter subjacente (None se não houver setter) |
ClassName.prop.fdel | A função deleter subjacente (None se não houver deleter) |