Vai al contenuto principale

Tutorial sui descriptor in Python

Scopri cosa sono i descriptor in Python, quando usarli e perché usarli.
Aggiornato 29 set 2026  · 13 min leggi

Scopri con l'IA

ChatGPTClaudePerplexity

I descriptor di Python, o più in generale i descriptor, ti offrono una potente tecnica per scrivere codice riutilizzabile che può essere condiviso tra classi. Possono sembrare simili al concetto di ereditarietà, ma tecnicamente non lo sono. Sono un modo generico per intercettare l’accesso agli attributi. I descriptor sono il meccanismo alla base di property, metodi statici, metodi di classe, super, ecc.

I descriptor sono stati aggiunti in Python dalla versione 2.2 e da allora sono considerati strumenti “magici” che hanno dato un nuovo stile alle classi tradizionali. Sono classi che ti permettono di gestire proprietà in un’altra classe. Nello specifico, implementano l’interfaccia per i metodi __get__(), __set__() e __delete__(), il che li rende interessanti per molti motivi. Per esempio, un class decorator e un property decorator che avrai già visto in Python.

In parole povere, una classe che implementa i metodi __get__(), __set()__ o __delete()__ per un oggetto è detta "Descriptor". Citando direttamente la documentazione ufficiale di Python, un descriptor è un attributo di oggetto con binding behavior, il cui accesso è stato ridefinito dai metodi del protocollo dei descriptor. Questi metodi sono __get__(), __set__() e __delete__() (Fonte).

Il binding behavior rispetto ai descriptor significa vincolare il modo in cui un valore può essere impostato, interrogato (get) o eliminato per una determinata variabile, oggetto o insieme di dati. Questa interazione completa è vincolata a quel pezzo di dato perché si applica solo ai dati su cui l’hai impostata, legandola quindi solo a quella specifica parte di dati.

I descriptor possono essere ulteriormente classificati in data e non-data descriptor. Se il descriptor che scrivi ha solo il metodo __get__(), allora è un non-data descriptor; invece, un’implementazione che coinvolge i metodi __set__() e __delete__() è chiamata data descriptor. I non-data descriptor sono solo leggibili, mentre i data descriptor sono sia leggibili che scrivibili.

È importante notare che i descriptor sono assegnati a una classe, non all’istanza di una classe. Modificare la classe sovrascrive o elimina il descriptor stesso, invece di attivarne il codice (IBM Developer).

Infine, la classe descriptor non è limitata ad avere solo questi tre metodi, il che significa che può anche contenere qualsiasi altro attributo oltre ai metodi get, set e delete.

Capiremo ora i metodi get, set e delete più nel dettaglio, ispirandoci a questa pagina di IBM Developer:

  • self è l’istanza del descriptor che crei (Real Python).
  • object è l’istanza dell’oggetto a cui è agganciato il descriptor (Real Python).
  • type è il tipo dell’oggetto a cui è agganciato il descriptor (Real Python).
  • value è il valore assegnato all’attributo del descriptor. get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() accede all’attributo o quando vuoi estrarre qualche informazione. Restituisce il valore dell’attributo o solleva l’eccezione AttributeError se l’attributo richiesto non è presente.

  • __set__() viene chiamato in un’operazione di assegnazione che imposta il valore di un attributo. Non restituisce nulla, ma può sollevare l’eccezione AttributeError.

  • __delete__() controlla un’operazione di eliminazione, cioè quando vuoi eliminare l’attributo da un oggetto. Non restituisce nulla.

Ora capiamo lo scopo dei Descriptor con l’aiuto di alcuni esempi!

Scopo dei descriptor

Definiamo una classe car che ha tre attributi, ovvero make, model e fuel_cap. Userai il metodo __init__() per inizializzare gli attributi della classe. Poi userai la funzione magica __str__(), che restituirà semplicemente l’output dei tre attributi che passerai alla classe durante la creazione dell’oggetto.

Nota che il metodo __str__() restituisce la rappresentazione testuale dell’oggetto. Viene chiamato quando si invoca la funzione print() o str() su un oggetto della classe.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

Come puoi vedere dall’output sopra, tutto sembra a posto!

Ora cambiamo la capacità del serbatoio dell’auto a meno 40.

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

Un attimo, c’è qualcosa che non va, vero? La capacità del serbatoio di un’auto non può mai essere negativa. Tuttavia, Python l’accetta come input senza alcun errore. Questo perché Python è un linguaggio dinamico che non supporta il type checking in modo esplicito.

Per evitare questo problema, aggiungiamo una condizione if nel metodo __init__() e verifichiamo se la capacità del serbatoio inserita è valida o meno. Se la capacità inserita è non valida, solleviamo un’eccezione ValueError.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

Dall’output sopra, puoi notare che per ora tutto funziona bene, dato che il programma solleva un ValueError se la capacità del serbatoio è sotto zero.

Tuttavia, può esserci un altro problema: e se la fuel capacity inserita fosse un valore float o una stringa? Non solo la capacità del serbatoio, ma anche la make e il model dell’auto potrebbero essere valori interi. In tutti questi casi, il programma non riuscirà a sollevare un’eccezione.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Per gestire anche il caso sopra, potresti pensare di aggiungere un’altra condizione if o magari usare il metodo isinstance per il type checking.

Questa volta usiamo la funzione built-in isinstance per gestire l’errore.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

Ottimo! Sei riuscito a gestire anche questo errore.

Tuttavia, cosa succede se in seguito vuoi cambiare esplicitamente l’attributo della capacità del serbatoio a meno 40? In questo caso non funzionerà, perché il type checking verrà eseguito solo una volta nel metodo __init__(). Come saprai, il metodo __init__() è un costruttore e viene chiamato una sola volta quando crei un oggetto della classe. Quindi, il controllo personalizzato del tipo fallirà in seguito.

Capciamolo con un esempio.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Ed ecco fatto! Sei riuscito ad aggirare il controllo del tipo.

Ora pensa a questo: e se avessi molti altri attributi dell’auto, come consumi, prezzo, accessori, ecc., che richiedono anch’essi il type checking, e volessi anche funzionalità per cui alcuni di questi attributi abbiano solo accesso in lettura? Non sarebbe fastidioso?

Ebbene, per affrontare tutti i problemi sopra, Python ha i Descriptor che vengono in tuo aiuto!

Come hai imparato sopra, qualsiasi classe che implementa i metodi magici __get__(), __set()__ o __delete()__ per un oggetto del protocollo dei descriptor è detta Descriptor. Ti danno anche un controllo aggiuntivo su come dovrebbe funzionare un attributo, per esempio se dovrebbe avere accesso in lettura o scrittura.

Ora estendiamo l’esempio sopra aggiungendo i metodi dei Descriptor di Python.

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

Niente panico se la classe Descriptor ti sembra oscura. Scomponiamola in piccoli pezzi e capiamo cosa fa essenzialmente ogni metodo.

  • Il metodo __init__() della classe Descriptor ha una variabile locale __fuel_cap impostata a zero. Il dunder, ovvero il doppio underscore all’inizio, significa che la variabile è privata. Avere un dunder all’inizio serve solo a distinguere l’attributo di capacità del serbatoio della classe Descriptor da quello della classe Car.
  • Come ormai sai, il metodo __get__() viene usato per recuperare gli attributi e restituisce la variabile della capacità del serbatoio. Accetta tre argomenti: l’oggetto descriptor, l’istanza della classe che contiene l’istanza del descriptor, cioè car2, e infine il proprietario, che è la classe a cui appartiene l’istanza, cioè la classe Car. In questo metodo, restituisci semplicemente l’attributo value, cioè fuel_cap, il cui valore è impostato nel metodo __set__().
  • Il metodo __set__() viene invocato quando si imposta il valore dell’attributo e, a differenza di __get__(), non restituisce nulla. Ha due argomenti oltre all’oggetto descriptor stesso, cioè l’istanza (come nel metodo __get__()) e l’argomento value, che è il valore che assegni all’attributo. In questo metodo verifichi se il valore che vuoi assegnare all’attributo fuel_cap è un intero o meno. In caso contrario, sollevi un’eccezione TypeError. Poi, nello stesso metodo, verifichi anche se il valore è minore di zero; se lo è, sollevi un’altra eccezione, questa volta un ValueError. Dopo i controlli, aggiorni l’attributo fuel_cap al valore passato.
  • Infine, il metodo __delete__(), che viene chiamato quando l’attributo viene eliminato da un oggetto e, come __set__(), non restituisce nulla.

La classe Car rimane la stessa di prima. Tuttavia, l’unico cambiamento che fai è aggiungere l’istanza fuel_cap della classe Descriptor(). Nota che, come detto in precedenza, l’istanza della classe descriptor deve essere aggiunta a una classe come attributo di classe e non come attributo di istanza.

Non appena imposti la variabile locale fuel_cap nel metodo __init__() all’istanza fuel_cap, viene invocato il metodo __set__() della classe Descriptor.

Ora cambiamo la capacità del serbatoio a un valore negativo e vediamo se il programma solleva un’eccezione ValueError oppure no.

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

Se ricordi qui, il type checking non aveva avuto successo quando avevi cambiato successivamente il valore dell’attributo a un numero negativo, poiché il controllo era applicato una sola volta nel metodo __init__(). Aggiorniamo ora il valore di fuel_cap a una stringa e vediamo se produce un errore.

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

Perfetto! Come vedi, funziona anche quando aggiorni successivamente l’attributo della capacità del serbatoio.

C’è però un piccolo problema nei Descriptor: quando crei una nuova istanza o una seconda istanza della classe, il valore dell’istanza precedente viene sovrascritto. Il motivo è che i Descriptor sono legati alla classe e non all’istanza.

Capimolo con l’esempio qui sotto.

car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values
48

Quando stampi l’istanza car2, noterai che i valori saranno stati sovrascritti da car3.

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

Conclusione

Congratulazioni per aver terminato il tutorial.

Questo tutorial è rivolto a chi si sente a proprio agio con Python e aspira a padroneggiarlo a un livello avanzato. Come buon esercizio, potresti voler capire come risolvere il problema della sovrascrittura tra istanze discusso oggi.

Per qualsiasi domanda relativa a questo tutorial, sentiti libero di scriverla nei commenti qui sotto.

Se vuoi imparare di più su Python, segui il percorso di competenze completo di DataCamp Python Programming.

Dai un’occhiata al nostro Tutorial sulle funzioni in Python.

Argomenti
Python

Corsi di Python

Corso

Introduzione a Python

4 ore
7M
Impara le basi dell’analisi dei dati con Python in sole 4 ore. Questo corso online ti introduce all’interfaccia di Python e ai pacchetti più usati.
Vedi i dettagliRight Arrow
Inizia Il Corso
Mostra altroRight Arrow
Correlato

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

Che cos'è Snowflake? Guida per principianti alla piattaforma dati cloud

Esplora le basi di Snowflake, la piattaforma dati cloud. Scopri la sua architettura, le sue funzionalità e come integrarla nelle tue pipeline di dati.
Tim Lu's photo

Tim Lu

12 min

blog

I 15 migliori server MCP remoti che ogni AI builder dovrebbe conoscere nel 2026

Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Abid Ali Awan's photo

Abid Ali Awan

15 min

Mostra AltroMostra Altro