Toda linguagem com campos privados te ensina a escrever get_x() e set_x() desde o início, caso você precise de validação depois. Python não precisa disso, e vale entender o motivo em vez de decorar.
O hábito de que o Python não precisa
# the Java habit, which Python does not need
class TemperatureJava:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
self._celsius = value
t = TemperatureJava(20)
t.set_celsius(25)
print(t.get_celsius())
Ele imprime 25. Seis linhas de cerimônia que não fazem nada além de deixar as chamadas mais feias.
Comece com um atributo comum
# in Python you start with a plain attribute
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t2 = Temperature(20)
t2.celsius = 25
print(t2.celsius)
Ele imprime 25. Mesmo comportamento, sem cerimônia.
A objeção é óbvia: e quando você precisar de validação? Numa linguagem sem properties a resposta é “você muda todas as chamadas”, e é por isso que as pessoas escrevem getters por precaução. Em Python a resposta é que você não muda.
Acrescente @property depois, e nada de fora muda
# and add @property later, without changing a single caller
class Temperature2:
def __init__(self, celsius):
self.celsius = celsius # goes through the setter below
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"{value} is below absolute zero")
self._celsius = value
t3 = Temperature2(20)
t3.celsius = 25
print(t3.celsius)
try:
t3.celsius = -300
except ValueError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
25
ValueError: -300 is below absolute zero
t3.celsius = 25 continua parecendo uma atribuição de atributo porque, sintaticamente, é uma. A property intercepta. Todas as chamadas que já existiam continuam funcionando sem mudança. É esse o argumento inteiro: você pode adiar a decisão até ter um motivo, porque tomá-la depois não custa nada.
Repare que o __init__ atribui self.celsius, não self._celsius. Isso faz a construção passar pelo setter, então um valor inválido é recusado na criação também, e não só numa atribuição posterior.
Properties calculadas
O outro uso, e o mais comum: um valor derivado de outros, que por isso nunca fica desatualizado.
# a computed property: derived, never stored, never stale
class Temperature3:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
t4 = Temperature3(100)
print(t4.fahrenheit)
t4.celsius = 0
print(t4.fahrenheit)
Ele imprime:
212.0
32.0
Guarde fahrenheit como atributo de verdade e você tem duas fontes de verdade que vão discordar na primeira vez que alguém mudar uma delas. Calculada, ela não consegue.
Somente leitura de graça
Deixe o setter de fora e a atribuição falha:
try:
t4.fahrenheit = 50
except AttributeError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
AttributeError: property 'fahrenheit' of 'Temperature3' object has no setter
Essa mensagem é do Python 3.11 em diante. Versões mais antigas dizem can't set attribute, que ajuda menos e é a mesma coisa.
O que lembrar
-
Comece com um atributo comum. Sempre.
-
Acrescente
@propertyquando realmente precisar de validação ou cálculo — quem chama nem percebe. -
Atribua pela property no
__init__para que a construção também seja validada. -
Uma property sem setter é somente leitura, que é o jeito mais barato de proteger um valor derivado.
A única coisa a não fazer é embrulhar todo atributo numa property “por precaução”. Isso é o hábito dos getters com sintaxe melhor, e custa a mesma clareza pelo mesmo nada.