Pourquoi la définition d'un descripteur sur une classe écrase-t-elle le descripteur?

10

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_getattrolaquelle j'ai trouvé en regardant "tp_slots" puis où tp_getattro est peuplé . Et le fait qu'imprime B.vinitialement 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_GenericSetAttrWithDictsemble 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 vplutô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_setattroce qui est appelé pendant B.v = 3.

Avertissement 2: VocalDescriptorn'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.

Michael Carilli
la source
1
Pour moi, cela affiche 3 à la dernière ligne ... Le code fonctionne bien
Jab
3
Les descripteurs s'appliquent lors de l'accès aux attributs à partir d'une instance , pas à la classe elle-même. Pour moi, le mystère est pourquoi __get__tout a fonctionné, plutôt que pourquoi __set__pas.
jasonharper
1
@Jab OP s'attend à toujours invoquer la __get__méthode. B.v = 3a effectivement remplacé l'attribut par un int.
r.ook
2
@jasonharper L'accès aux attributs détermine s'il __get__est appelé, ainsi que les implémentations par défaut object.__getattribute__et type.__getattribute__invoquées __get__lors de l'utilisation d'une instance ou de la classe. L'affectation via __set__est uniquement une instance.
chepner
@jasonharper Je crois que les __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 pourquoi B.v = 3est capable d'écraser le descripteur de classe. Sur la base de l'implémentation de CPython, je m'attendais B.v = 3à déclencher également __set__.
Michael Carilli

Réponses:

6

Vous avez raison qui B.v = 3remplace simplement le descripteur par un entier (comme il se doit).

Pour B.v = 3invoquer un descripteur, le descripteur aurait dû être défini sur la métaclasse, c'est-à-dire sur type(B).

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

Pour invoquer le descripteur B, vous utiliseriez une instance: le B().v = 3fera.

La raison de l' B.vinvocation 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:

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__')

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 état B.v.__dict__est partagé entre toutes les instances de B.

Bien sûr, il appartient au code de l'utilisateur de définir exactement ce qu'il veut B.vfaire, le retour selfn'est que le modèle courant.

wim
la source
1
Pour compléter cette réponse, j'ajouterais qu'il __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__
sanyash
@wim Magnificent !! En parallèle, je regardais une fois de plus la chaîne d'appels type_setattro. Je vois que l'appel à _PyObject_GenericSetAttrWithDict fournit le type (à ce point B, dans mon cas).
Michael Carilli
À l'intérieur _PyObject_GenericSetAttrWithDict, il tire le Py_TYPE de B as tp, qui est la métaclasse de B ( typedans mon cas), puis c'est la métaclasse tp qui est traitée par la logique de court-circuitage des descripteurs . Ainsi, le descripteur défini directement sur B 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.
Michael Carilli
Par conséquent, dans votre cas où la métaclasse a un descripteur, la __set__méthode de ce descripteur est appelée.
Michael Carilli
@sanyash n'hésitez pas à modifier directement.
wim
3

Sauf toute substitution, B.vest équivalent à type.__getattribute__(B, "v"), tandis que b = B(); b.vest é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.vpasse Noneen premier argument, alors que B().vpasse l'instance elle - même. Dans les deux cas, Ble deuxième argument est transmis.

B.v = 3, d'autre part, est équivalent à type.__setattr__(B, "v", 3), qui n'invoque pas__set__ .

chepner
la source