Repro simple:
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
Cette question a un doublon efficace , mais le double n'a pas été répondu, et j'ai creusé un peu plus dans la source CPython comme exercice d'apprentissage. Attention: je suis entré dans les mauvaises herbes. J'espère vraiment pouvoir obtenir l'aide d' un capitaine qui connaît ces eaux . J'ai essayé d'être aussi explicite que possible en traçant les appels que je regardais, pour mon propre avantage futur et celui des futurs lecteurs.
J'ai vu beaucoup d'encre renversée sur le comportement des __getattribute__
appliqués aux descripteurs, par exemple la priorité de recherche. L'extrait Python dans "Invoking Descriptors" juste en dessous For classes, the machinery is in type.__getattribute__()...
correspond à peu près dans mon esprit avec ce que je pense être la source CPython correspondante dans type_getattro
laquelle j'ai trouvé en regardant "tp_slots" puis où tp_getattro est peuplé . Et le fait qu'imprime B.v
initialement ait du __get__, obj=None, objtype=<class '__main__.B'>
sens pour moi.
Ce que je ne comprends pas, c'est pourquoi l'affectation B.v = 3
écrase aveuglément le descripteur, plutôt que de se déclencher v.__set__
? J'ai essayé de tracer l'appel CPython, en recommençant à partir de "tp_slots" , puis en regardant où tp_setattro est rempli , puis en regardant type_setattro . type_setattro
semble être une enveloppe mince autour de _PyObject_GenericSetAttrWithDict . Et il y a le nœud de ma confusion: _PyObject_GenericSetAttrWithDict
semble avoir une logique qui donne la priorité à la __set__
méthode d' un descripteur !! Dans cet esprit, je ne peux pas comprendre pourquoi B.v = 3
écrase aveuglément v
plutôt que de déclencher v.__set__
.
Avertissement 1: Je n'ai pas reconstruit Python à partir de la source avec printfs, donc je ne suis pas complètement sûr de type_setattro
ce qui est appelé pendant B.v = 3
.
Avertissement 2: VocalDescriptor
n'est pas destiné à illustrer la définition de descripteur "typique" ou "recommandé". C'est un non-verbeux pour me dire quand les méthodes sont appelées.
la source
__get__
tout a fonctionné, plutôt que pourquoi__set__
pas.__get__
méthode.B.v = 3
a effectivement remplacé l'attribut par unint
.__get__
est appelé, ainsi que les implémentations par défautobject.__getattribute__
ettype.__getattribute__
invoquées__get__
lors de l'utilisation d'une instance ou de la classe. L'affectation via__set__
est uniquement une instance.__get__
méthodes des descripteurs sont censées se déclencher lorsqu'elles sont appelées à partir de la classe elle-même. C'est ainsi que @classmethods et @staticmethods sont implémentés, selon le guide pratique . @Jab Je me demande pourquoiB.v = 3
est capable d'écraser le descripteur de classe. Sur la base de l'implémentation de CPython, je m'attendaisB.v = 3
à déclencher également__set__
.Réponses:
Vous avez raison qui
B.v = 3
remplace simplement le descripteur par un entier (comme il se doit).Pour
B.v = 3
invoquer un descripteur, le descripteur aurait dû être défini sur la métaclasse, c'est-à-dire surtype(B)
.Pour invoquer le descripteur
B
, vous utiliseriez une instance: leB().v = 3
fera.La raison de l'
B.v
invocation du getter est de permettre le retour de l'instance de descripteur elle-même. Habituellement, vous feriez cela, pour autoriser l'accès au descripteur via l'objet classe:Maintenant
B.v
, retournerait une instance comme<mymodule.VocalDescriptor object at 0xdeadbeef>
laquelle vous pouvez interagir. C'est littéralement l'objet descripteur, défini comme un attribut de classe, et son étatB.v.__dict__
est partagé entre toutes les instances deB
.Bien sûr, il appartient au code de l'utilisateur de définir exactement ce qu'il veut
B.v
faire, le retourself
n'est que le modèle courant.la source
__get__
est conçu pour être appelé comme attribut d'instance ou attribut de classe, mais__set__
est conçu pour être appelé uniquement comme attribut d'instance. Et les documents pertinents: docs.python.org/3/reference/datamodel.html#object.__get___PyObject_GenericSetAttrWithDict
, il tire le Py_TYPE de B astp
, qui est la métaclasse de B (type
dans mon cas), puis c'est la métaclassetp
qui est traitée par la logique de court-circuitage des descripteurs . Ainsi, le descripteur défini directement surB
n'est pas vu par cette logique de court-circuit (donc dans mon code d'origine__set__
n'est pas appelé), mais un descripteur défini sur la métaclasse est vu par la logique de court-circuit.__set__
méthode de ce descripteur est appelée.Sauf toute substitution,
B.v
est équivalent àtype.__getattribute__(B, "v")
, tandis queb = B(); b.v
est équivalent àobject.__getattribute__(b, "v")
. Les deux définitions invoquent la__get__
méthode du résultat s'il est défini.Notez, pensait, que l'appel à
__get__
diffère dans chaque cas.B.v
passeNone
en premier argument, alors queB().v
passe l'instance elle - même. Dans les deux cas,B
le deuxième argument est transmis.B.v = 3
, d'autre part, est équivalent àtype.__setattr__(B, "v", 3)
, qui n'invoque pas__set__
.la source