Warum überschreibt das Festlegen eines Deskriptors für eine Klasse den Deskriptor?

10

Einfache Reproduktion:

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

Diese Frage hat ein effektives Duplikat , aber das Duplikat wurde nicht beantwortet, und ich habe mich als Lernübung etwas mehr mit der CPython-Quelle befasst. Achtung: Ich bin ins Unkraut gegangen. Ich hoffe wirklich, dass ich Hilfe von einem Kapitän bekommen kann, der diese Gewässer kennt . Ich habe versucht, die Anrufe, die ich mir ansah, so explizit wie möglich zu verfolgen, zu meinem eigenen zukünftigen Nutzen und zum Nutzen zukünftiger Leser.

Ich habe viel Tinte über das Verhalten von __getattribute__Deskriptoren verschüttet gesehen , z. B. Vorrang bei der Suche. Die Python - Code - Schnipsel in „Hervorrufen Descriptors“ knapp unter For classes, the machinery is in type.__getattribute__()...etwa in meinem Kopf stimmt , was ich glaube , dass die entsprechende CPython Quelle in type_getattro, die ich durch einen Blick auf aufgespürt „tp_slots“ dann wo tp_getattro bevölkert ist . Und die Tatsache, dass B.vzunächst gedruckt wird, __get__, obj=None, objtype=<class '__main__.B'>macht für mich Sinn.

Was ich nicht verstehe ist, warum B.v = 3überschreibt die Zuweisung den Deskriptor blind, anstatt ihn auszulösen v.__set__? Ich habe versucht, den CPython-Aufruf zu verfolgen, indem ich noch einmal von "tp_slots" aus angefangen habe , dann nachgesehen habe , wo tp_setattro gefüllt ist , und dann nach type_setattro . type_setattro scheint ein dünner Wrapper um _PyObject_GenericSetAttrWithDict zu sein . Und da ist der Kern meiner Verwirrung: Es _PyObject_GenericSetAttrWithDictscheint eine Logik__set__ zu haben , die der Methode eines Deskriptors Vorrang einräumt !! Vor diesem Hintergrund kann ich nicht herausfinden, warum B.v = 3blind überschrieben vund nicht ausgelöst wird v.__set__.

Haftungsausschluss 1: Ich habe Python nicht mit printfs aus dem Quellcode neu erstellt, daher bin ich mir nicht ganz sicher, type_setattrowie es während des Aufrufs aufgerufen wird B.v = 3.

Haftungsausschluss 2: VocalDescriptorsoll nicht als Beispiel für eine "typische" oder "empfohlene" Deskriptordefinition dienen. Es ist ein ausführliches No-Op, mir zu sagen, wann die Methoden aufgerufen werden.

Michael Carilli
quelle
1
Für mich druckt dies 3 in der letzten Zeile ... Der Code funktioniert gut
Jab
3
Deskriptoren gelten beim Zugriff auf Attribute von einer Instanz aus , nicht von der Klasse selbst. Für mich ist das Rätsel, warum __get__überhaupt gearbeitet hat und nicht warum __set__.
Jasonharper
1
@Jab OP erwartet, dass die __get__Methode weiterhin aufgerufen wird . B.v = 3hat das Attribut effektiv mit einem überschrieben int.
r.ook
2
Zugang @jasonharper Attribut legt fest , ob __get__genannt wird , und die Standard - Implementierungen object.__getattribute__und type.__getattribute__invoke , __get__wenn eine Instanz oder die Klasse. Das Zuweisen über __set__ist nur eine Instanz.
Chepner
@jasonharper Ich glaube, die __get__Methoden der Deskriptoren sollen ausgelöst werden, wenn sie von der Klasse selbst aufgerufen werden. Auf diese Weise werden @classmethods und @staticmethods gemäß der Anleitung implementiert . @Jab Ich frage mich, warum B.v = 3der Klassendeskriptor überschrieben werden kann. Basierend auf der CPython-Implementierung hatte ich erwartet B.v = 3, auch auszulösen __set__.
Michael Carilli

Antworten:

6

Sie haben Recht, dass B.v = 3der Deskriptor einfach mit einer Ganzzahl überschrieben wird (wie es sollte).

Um B.v = 3einen Deskriptor aufzurufen, sollte der Deskriptor in der Metaklasse definiert worden sein, dh in type(B).

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

Um den Deskriptor aufzurufen B, würden Sie eine Instanz verwenden: B().v = 3wird es tun.

Der Grund für das B.vAufrufen des Getters besteht darin, die Rückgabe der Deskriptorinstanz selbst zuzulassen. Normalerweise würden Sie dies tun, um den Zugriff auf den Deskriptor über das Klassenobjekt zu ermöglichen:

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

Jetzt B.vwürde eine Instanz zurückgegeben, mit <mymodule.VocalDescriptor object at 0xdeadbeef>der Sie interagieren können. Es ist buchstäblich das Deskriptorobjekt, das als Klassenattribut definiert ist, und sein Status B.v.__dict__wird von allen Instanzen von gemeinsam genutzt B.

Natürlich liegt es am Code des Benutzers, genau zu definieren, was B.ver tun möchte. Die Rückgabe selfist nur das übliche Muster.

wim
quelle
1
Um diese Antwort zu vervollständigen, würde ich hinzufügen, dass __get__sie als Instanzattribut oder Klassenattribut aufgerufen werden soll, aber __set__nur als Instanzattribut. Und relevante Dokumente: docs.python.org/3/reference/datamodel.html#object.__get__
Sanyash
@wim Magnificent !! Parallel dazu habe ich mir noch einmal die Aufrufkette type_setattro angesehen. Ich sehe, dass der Aufruf von _PyObject_GenericSetAttrWithDict den Typ liefert (in diesem Fall an diesem Punkt B).
Michael Carilli
Innerhalb _PyObject_GenericSetAttrWithDictes die Py_TYPE von B zieht , wie tp, die Metaklasse B ist ( typein meinem Fall), dann ist es die Metaklasse tp , die durch die behandelt wird Deskriptors kurzzuschließen Logik . Der direkt definierte Deskriptor B wird also von dieser Kurzschlusslogik nicht gesehen (daher wird in meinem ursprünglichen Code __set__nicht aufgerufen), aber ein in der Metaklasse definierter Deskriptor wird von der Kurzschlusslogik gesehen.
Michael Carilli
Daher ist in Ihrem Fall , in dem die Metaklasse einen Beschreiber hat, das __set__Verfahren dieses Descriptor wird genannt.
Michael Carilli
@sanyash zögern Sie nicht, direkt zu bearbeiten.
wim
3

Das Ausschließen von Überschreibungen B.vist äquivalent zu type.__getattribute__(B, "v"), während b = B(); b.väquivalent zu ist object.__getattribute__(b, "v"). Beide Definitionen rufen die __get__Methode des Ergebnisses auf, falls definiert.

Beachten Sie, dass der Aufruf von __get__in jedem Fall unterschiedlich ist. B.vwird Noneals erstes Argument übergeben, während B().vdie Instanz selbst übergeben wird. In beiden Fällen Bwird als zweites Argument übergeben.

B.v = 3ist andererseits äquivalent zu type.__setattr__(B, "v", 3), was nicht aufruft __set__.

chepner
quelle