Perché l'impostazione di un descrittore su una classe sovrascrive il descrittore?


10

Riproduzione semplice:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

Questa domanda ha un duplicato efficace , ma al duplicato non è stata data risposta e ho scavato un po 'di più nella fonte CPython come esercizio di apprendimento. Attenzione: sono andato tra le erbacce. Spero davvero di poter ottenere aiuto da un capitano che conosce quelle acque . Ho cercato di essere il più esplicito possibile nel tracciare le chiamate che stavo guardando, per i miei benefici futuri e per i futuri lettori.

Ho visto molto inchiostro rovesciato sul comportamento __getattribute__applicato ai descrittori, ad esempio la precedenza di ricerca. Il Python snippet in "Richiamo descrittori" appena al di sotto For classes, the machinery is in type.__getattribute__()...d'accordo o meno nella mia mente con quello che credo è il corrispondente fonte CPython in type_getattro, che ho rintracciato, cercando in "tp_slots" quindi dove tp_getattro è popolato . E il fatto che B.vinizialmente le stampe __get__, obj=None, objtype=<class '__main__.B'>abbiano senso per me.

Quello che non capisco è, perché il compito B.v = 3sovrascrive ciecamente il descrittore, anziché innescarlo v.__set__? Ho provato a rintracciare la chiamata CPython, partendo ancora una volta da "tp_slots" , quindi guardando dove è popolata tp_setattro , quindi guardando type_setattro . type_setattro sembra essere un wrapper sottile attorno a _PyObject_GenericSetAttrWithDict . E c'è il nocciolo della mia confusione: _PyObject_GenericSetAttrWithDictsembra avere una logica che dà la precedenza al __set__metodo di un descrittore !! Con questo in mente, non riesco a capire perché B.v = 3sovrascrive ciecamente vinvece di innescare v.__set__.

Dichiarazione di non responsabilità 1: non ho ricostruito Python dal sorgente con printfs, quindi non sono del tutto sicuro di type_setattrocome venga chiamato durante B.v = 3.

Dichiarazione di VocalDescriptornon responsabilità 2: non ha lo scopo di esemplificare la definizione di descrittore "tipica" o "raccomandata". È una no-op verbosa dirmi quando vengono chiamati i metodi.


1
Per me questo stampa 3 nell'ultima riga ... Il codice funziona bene
Jab

3
I descrittori si applicano quando si accede agli attributi da un'istanza , non alla classe stessa. Per me, il mistero è perché ha __get__funzionato affatto, piuttosto che perché __set__non ha funzionato.
Jasonharper,

1
@Jab OP prevede di invocare ancora il __get__metodo. B.v = 3ha effettivamente sovrascritto l'attributo con un int.
Guarda il

2
@jasonharper L'accesso agli attributi determina se __get__viene chiamato e le implementazioni predefinite object.__getattribute__e type.__getattribute__invocano __get__quando si utilizza un'istanza o la classe. L'assegnazione tramite __set__è solo istanza.
Chepner,

@jasonharper Credo che i __get__metodi dei descrittori dovrebbero innescarsi quando invocati dalla classe stessa. Ecco come vengono implementati @classmethods e @staticmethods, secondo la guida pratica . @Jab Mi chiedo perché B.v = 3sia in grado di sovrascrivere il descrittore di classe. Sulla base dell'implementazione di CPython, mi aspettavo B.v = 3di innescare anche __set__.
Michael Carilli,

Risposte:


6

Hai ragione che B.v = 3sovrascrive semplicemente il descrittore con un numero intero (come dovrebbe).

Per B.v = 3invocare un descrittore, il descrittore avrebbe dovuto essere definito sulla metaclasse, cioè su type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

Per invocare il descrittore B, dovresti usare un'istanza: B().v = 3lo farà.

Il motivo per B.vinvocare il getter è consentire la restituzione dell'istanza del descrittore stessa. Di solito lo faresti, per consentire l'accesso al descrittore tramite l'oggetto class:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

Ora B.vritornerebbe qualche istanza come la <mymodule.VocalDescriptor object at 0xdeadbeef>quale puoi interagire. È letteralmente l'oggetto descrittore, definito come un attributo di classe, e il suo stato B.v.__dict__è condiviso tra tutte le istanze di B.

Ovviamente spetta al codice dell'utente definire esattamente ciò che vogliono B.vfare, il ritorno selfè solo il modello comune.


1
Per completare questa risposta, aggiungerei che __get__è progettato per essere chiamato come attributo di istanza o attributo di classe, ma __set__è progettato per essere chiamato solo come attributo di istanza. E documenti pertinenti: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash,

@wim Magnificent !! Parallelamente stavo guardando ancora una volta la catena di chiamate type_setattro. Vedo che la chiamata a _PyObject_GenericSetAttrWithDict fornisce il tipo (a quel punto B, nel mio caso).
Michael Carilli,

All'interno _PyObject_GenericSetAttrWithDict, estrae il Py_TYPE di B as tp, che è la metaclasse di B ( typenel mio caso), quindi è la metaclasse tp che viene trattata dalla logica di cortocircuito del descrittore . Quindi il descrittore definito direttamente B non è visto da quella logica di cortocircuito (quindi nel mio codice originale __set__non è chiamato), ma un descrittore definito sulla metaclasse è visto dalla logica di cortocircuito.
Michael Carilli,

Pertanto, nel tuo caso in cui la metaclasse ha un descrittore, viene chiamato il __set__metodo di quel descrittore .
Michael Carilli,

@sanyash sentiti libero di modificare direttamente.
mercoledì

3

Blocco di eventuali sostituzioni, B.vequivale a type.__getattribute__(B, "v"), mentre b = B(); b.vequivale a object.__getattribute__(b, "v"). Entrambe le definizioni invocano il __get__metodo del risultato se definito.

Nota, pensato, che la chiamata a __get__differisce in ogni caso. B.vpassa Nonecome primo argomento, mentre B().vpassa l'istanza stessa. In entrambi i casi Bviene passato come secondo argomento.

B.v = 3, d'altra parte, è equivalente a type.__setattr__(B, "v", 3), che non invoca __set__.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.