W3docs

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 impossible

Il 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._celsius

Il 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 automatically

Poiché non esiste un setter, tentare di assegnarvi un valore genera un errore:

t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setter

Questo è 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 = value

Ora 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 zero

Regola 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 + 32

fahrenheit 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.0

Poiché 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 positive

Aggiungere 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 = -1

In 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 negative

Qualsiasi 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 years

La 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)     # None

Ecco 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 again

Memorizza 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 attribute

Setter 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 negative

Le 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

SintassiCosa fa
@propertyDefinisce il getter; l'attributo diventa di sola lettura finché non viene aggiunto un setter
@<name>.setterDefinisce il setter; l'attributo diventa leggibile e scrivibile
@<name>.deleterDefinisce il deleter; del obj.attr attiva questo metodo
property(fget, fset, fdel, doc)Equivalente integrato senza sintassi del decoratore
ClassName.prop.fgetLa funzione getter sottostante
ClassName.prop.fsetLa funzione setter sottostante (None se non c'è setter)
ClassName.prop.fdelLa funzione deleter sottostante (None se non c'è deleter)

Esercitazione

Pratica
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Pratica
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Pratica
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?