Storia.
Prima di Python 3.6, i descriptor che richiedevano la conoscenza del loro nome di attributo dipendevano da metaclassi personalizzate o decoratori di classe manuali per esaminare il dizionario della classe e iniettare nomi. Questo approccio era verbose, soggetto a errori, e creava conflitti di metaclassi in gerarchie complesse. PEP 487 ha introdotto il protocollo __set_name__ in Python 3.6 per eliminare questo boilerplate consentendo all'interprete di notificare automaticamente i descriptor.
Problema.
Un'istanza del descriptor viene creata durante l'esecuzione del corpo della classe, ma in quel momento non ha conoscenza intrinseca del nome della variabile a cui è associata o della classe in cui risiede. Queste informazioni sono essenziali per generare messaggi di errore significativi, registrare campi nei sistemi ORM, o costruire schemi di serializzazione. Senza una notifica esterna, il descriptor rimane anonimo, costringendo gli sviluppatori a ripetere il nome dell'attributo come argomento stringa, violando i principi DRY.
Soluzione.
Quando type.__new__ costruisce una classe, itera sulla mappatura dello spazio dei nomi restituita da __prepare__. Per ogni valore che possiede un metodo __set_name__, l'interprete invoca value.__set_name__(owner_class, attribute_name). Questo metodo riceve la classe in fase di costruzione e la stringa dell'attributo, permettendo al descriptor di memorizzare questi metadati. Tuttavia, se un descriptor viene assegnato a un attributo di classe dopo che il processo di creazione della classe è completo (monkey-patching), __set_name__ non viene invocato automaticamente perché la macchina dei tipi non è più attiva.
class TrackedDescriptor: def __set_name__(self, owner, name): self.owner = owner self.name = name def __get__(self, instance, owner): if instance is None: return self return f"{self.owner.__name__}.{self.name}" class Model: field = TrackedDescriptor() # Model.field.name == 'field' # Model.field.owner == Model
Contesto.
Durante lo sviluppo di una libreria di gestione della configurazione, avevamo bisogno di descriptor per rappresentare le variabili ambientali. Quando un valore era mancante o non valido, l'errore doveva specificare il nome esatto dell'attributo nella classe (ad esempio, Config.database_url è richiesto), non solo un messaggio generico.
Problema.
Inizialmente, gli utenti dovevano specificare il nome manualmente: database_url = EnvVar('database_url'). Questo portava a bug durante il refactoring in cui la stringa literale e il nome della variabile divergevano, causando errori di runtime criptici.
Diverse soluzioni considerate:
Iniezione di metaclassi. Abbiamo implementato un ConfigMeta che ispezionava attrs e chiamava attr.set_name(name) su ciascun descriptor. Questo funzionava ma costringeva tutte le classi utente ad ereditare dalla nostra metaclasse, rompendo la compatibilità con altre librerie che usano le proprie metaclassi come abc.ABCMeta. Ha anche aggiunto un sovraccarico cognitivo per gli utenti non familiari con le metaclassi.
Patching del decoratore di classe. Abbiamo creato un decoratore @config che iterava su cls.__dict__ dopo la creazione della classe e patchava i nomi. Questo evitava conflitti di metaclassi ma era facoltativo; dimenticarsi del decoratore portava a descriptor rotti. Eseguiva anche dopo la creazione della classe, quindi i descriptor non potevano usare i loro nomi durante i ganci __init_subclass__, limitando le capacità di introspezione.
Protocollo __set_name__. Abbiamo aggiunto __set_name__ al nostro descriptor EnvVar. Questo non richiedeva cambiamenti nel codice degli utenti, funzionava automaticamente durante la definizione della classe, e permetteva al descriptor di conoscere il suo nome prima che __init_subclass__ fosse completato, abilitando la validazione anticipata.
Soluzione scelta.
Abbiamo adottato __set_name__ perché forniva un'astrazione a costo zero per gli utenti e si integrava con il modello di dati nativo di Python. Ha eliminato completamente il problema di collisione delle metaclassi.
Risultato.
L'API è diventata dichiarativa: database_url = EnvVar(). Gli strumenti di refactoring potevano rinominare gli attributi in modo sicuro e i messaggi di errore rimanevano accurati. Il codice si è ridotto di 150 righe di boilerplate delle metaclassi, e abbiamo osservato meno segnalazioni di bug relative a discrepanze nei tasti di configurazione.
Quando esattamente viene invocato __set_name__ durante il ciclo di vita della creazione della classe?
Viene invocato da type.__new__ immediatamente dopo che il corpo della classe ha terminato l'esecuzione e il dizionario dello spazio dei nomi è popolato, ma prima che __init_subclass__ venga chiamato sulle classi genitore. Questo tempismo è critico perché consente ai descriptor di finalizzare il loro stato prima che le sottoclassi siano inizializzate. Non viene attivato quando si aggiungono attributi a una classe già creata (ad esempio, setattr(MyClass, 'new_attr', descriptor())), perché il protocollo di creazione della classe è concluso. Comprendere questa distinzione è vitale per la manipolazione dinamica delle classi.
Perché __set_name__ riceve sia la classe proprietaria che il nome come argomenti invece di dedurli da self?
L'istanza del descriptor esiste indipendentemente dalla classe; può essere istanziata prima della creazione della classe e teoricamente potrebbe essere assegnata a più classi (anche se raro). L'argomento owner garantisce che il descriptor conosca la specifica classe in cui è avvenuta l'assegnazione, il che è necessario per gestire correttamente l'ereditarietà. Se un descriptor è definito in una classe base, __set_name__ viene chiamato con la classe base; se sovrascritto in una sottoclasse con una nuova istanza, viene chiamato con la sottoclasse. Questo consente registri per classe senza contaminazione incrociata tra classi base e derivate.
Come interagisce __set_name__ con i metodi del protocollo descriptor __set__ e __get__?
__set_name__ è puramente un gancio di inizializzazione e non partecipa al protocollo di accesso agli attributi (__get__/__set__). Tuttavia, consente a quei metodi di funzionare correttamente fornendo il contesto necessario per le operazioni. Un errore comune è presumere che __set_name__ verrà chiamato di nuovo quando un descriptor viene ereditato da una sottoclasse che non lo sovrascrive. Poiché la stessa istanza del descriptor viene riutilizzata, __set_name__ non viene rievocato; pertanto, i descriptor che tracciano lo stato per classe devono utilizzare __init_subclass__ o controllare owner in __get__ per gestire l'ereditarietà, piuttosto che fare affidamento esclusivamente su __set_name__ per la logica specifica della sottoclasse.