Python @property: Getter e Setter
Scopri il decoratore @property di Python: crea getter, setter, deleter e attributi calcolati con sintassi pulita e pieno controllo sulla validazione.
Il decoratore @property è il meccanismo integrato di Python per trasformare un metodo in un attributo gestito. Invece di scrivere metodi get_x() e set_x() come in altri linguaggi, si scrive un accesso all'attributo dall'aspetto normale (obj.x) mantenendo il pieno controllo su ciò che accade quando quell'attributo viene letto, scritto o eliminato.
Questo capitolo tratta:
- Perché esistono le property e quando usarle
- Creare una property di sola lettura con
@property - Aggiungere un setter con
@<name>.setter - Aggiungere un deleter con
@<name>.deleter - Property calcolate (derivate)
- Aggiornare un attributo semplice a una property senza modificare il codice chiamante
- La funzione integrata
property()— il meccanismo sottostante al decoratore - Come funzionano le property come descrittori (breve sguardo sotto il cofano)
- Errori comuni
Prima di leggere, assicurati di avere dimestichezza con classi e oggetti Python. Le property sono uno strumento fondamentale per l'incapsulamento in Python. Per i metodi a livello di classe e statici, vedere @staticmethod e @classmethod.
Perché Esistono le Property
Considera una classe che memorizza una temperatura in gradi Celsius. Un'implementazione naive espone direttamente il valore interno:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t = Temperature(25)
t.celsius = -5000 # nothing stops this — physically impossibleIl problema: niente impedisce ai chiamanti di impostare una temperatura al di sotto dello zero assoluto (−273,15 °C). Si potrebbe aggiungere un metodo set_celsius() con validazione, ma i chiamanti dovrebbero cambiare il loro codice da t.celsius = 100 a t.set_celsius(100) — una modifica che rompe l'API.
@property risolve questo problema in modo pulito. Si mantiene la sintassi t.celsius = 100 aggiungendo un livello di controllo dietro le quinte.
Getter di Base: Accesso in Sola Lettura
L'uso più semplice di @property è un attributo di sola lettura supportato da una variabile privata:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # store in a private attribute
@property
def celsius(self):
return self._celsiusIl decoratore @property fa apparire celsius come un attributo semplice per il chiamante:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses; Python calls the getter automaticallyPoiché non esiste un setter, tentare di assegnarvi un valore genera un errore:
t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setterQuesto è il modo corretto per modellare un valore che deve essere impostato solo al momento della costruzione o tramite metodi specifici.
Aggiungere un Setter con Validazione
Decora un secondo metodo con @<property_name>.setter per gestire le scritture:
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 = valueOra sia la lettura che la scrittura funzionano con la sintassi degli attributi semplici:
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 100
print(t.celsius) # 100
t.celsius = -300 # ValueError: Temperature below absolute zeroRegola fondamentale: il setter e il getter devono condividere lo stesso nome (celsius in entrambi i casi). Il decoratore @celsius.setter collega il nuovo metodo all'oggetto property celsius esistente.
Property Calcolate
Una property non deve necessariamente corrispondere a un attributo memorizzato. Può calcolare un valore al volo a partire da altri dati:
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 non ha una variabile di supporto — deriva il suo valore da _celsius ogni volta che viene letta:
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.celsius = 100
print(t.fahrenheit) # 212.0Poiché non esiste @fahrenheit.setter, tentare di scrivere t.fahrenheit = 100 genera un AttributeError. Le property calcolate sono naturalmente di sola lettura, a meno che non si aggiunga esplicitamente un setter.
Un esempio reale di property calcolata
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 positiveAggiungere un Deleter
Il decoratore @<property_name>.deleter consente di eseguire del codice quando il chiamante 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'I deleter sono usati meno comunemente rispetto a getter e setter. Sono utili quando:
- Si rimuove un valore dalla cache per forzare il ricalcolo al prossimo accesso.
- Si rilasciano esplicitamente risorse legate a un attributo.
- Si vuole garantire che, una volta eliminato, un valore non possa essere riletto senza essere riassegnato.
Aggiornare un Attributo Semplice a una Property
Uno dei maggiori vantaggi pratici di @property è che si può iniziare con un attributo pubblico semplice e aggiungere la validazione in seguito senza modificare il codice chiamante. Questo principio viene talvolta chiamato principio di accesso 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 = -1In seguito si ha bisogno di validazione. Con @property si può aggiungerla senza modificare i chiamanti:
# 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 negativeQualsiasi codice esistente che legge o scrive c.radius continua a funzionare senza modifiche.
La Funzione Integrata property()
@property è zucchero sintattico per la funzione integrata property(). Queste due definizioni sono equivalenti:
# --- 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) accetta fino a quattro argomenti: una funzione getter, una funzione setter, una funzione deleter e una docstring. Ognuno di essi può essere None.
p = Person(30)
print(p.age) # 30
p.age = 31
print(p.age) # 31
print(Person.age.__doc__) # The person's age in yearsLa forma con decoratore è più pulita ed è la raccomandazione standard. La chiamata esplicita a property() è utile quando si vuole passare la docstring senza un blocco decoratore su più righe, o quando le funzioni accessor esistono già con un altro nome.
Come Funzionano le Property: Breve Sguardo ai Descrittori
Internamente, property è un descrittore — un object che definisce __get__, __set__ e __delete__ sulla classe. Quando Python cerca obj.attr, controlla se l'attributo sulla classe è un descrittore e, in tal caso, chiama il suo __get__ invece di restituire il valore direttamente.
È possibile verificarlo ispezionando l'object property sulla 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) # NoneEcco perché leggere Square.side restituisce l'object property stesso (descrittore a cui si accede sulla classe), mentre leggere s.side su un'istanza attiva __get__ e restituisce il numero intero. Il protocollo dei descrittori è lo stesso meccanismo usato da classmethod, staticmethod e dalle funzioni stesse. Per un approfondimento, vedere metodi magici di Python.
Errori Comuni
Ricorsione infinita: dimenticare il carattere di sottolineatura
Un errore molto comune è usare lo stesso nome sia per la property che per l'attributo di supporto:
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 againMemorizza sempre il valore di supporto in un nome diverso, per convenzione preceduto da un carattere di sottolineatura:
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 definito prima del getter
Il decoratore setter @celsius.setter fa riferimento all'object property celsius, che deve esistere prima. Definisci sempre il getter (@property) prima del setter e del deleter nel corpo della classe.
__init__ chiama il setter automaticamente
Quando si scrive self.radius = radius all'interno di __init__, Python chiama il setter (se ne esiste uno). Di solito è ciò che si desidera — la validazione viene eseguita anche al momento della costruzione. Ma significa che il setter deve gestire correttamente l'assegnazione iniziale:
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 negativeLe property sono a livello di classe, non di istanza
Non è possibile aggiungere una property a una singola istanza come si farebbe con gli attributi regolari. Le property sono definite sulla classe e si applicano a tutte le istanze. Se hai bisogno di personalizzazione degli attributi per istanza, vedi dataclass Python o usa un approccio basato su __slots__.
Riferimento Rapido
| Sintassi | Cosa fa |
|---|---|
@property | Definisce il getter; l'attributo diventa di sola lettura finché non viene aggiunto un setter |
@<name>.setter | Definisce il setter; l'attributo diventa leggibile e scrivibile |
@<name>.deleter | Definisce il deleter; del obj.attr attiva questo metodo |
property(fget, fset, fdel, doc) | Equivalente integrato senza sintassi del decoratore |
ClassName.prop.fget | La funzione getter sottostante |
ClassName.prop.fset | La funzione setter sottostante (None se non c'è setter) |
ClassName.prop.fdel | La funzione deleter sottostante (None se non c'è deleter) |