Dlaczego ustawienie deskryptora w klasie zastępuje deskryptor?

10

Proste repro:

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

To pytanie ma skuteczny duplikat , ale na duplikat nie udzielono odpowiedzi, a jako ćwiczenie edukacyjne wkopałem trochę więcej w źródło CPython. Ostrzeżenie: poszedłem do chwastów. Naprawdę mam nadzieję, że uda mi się uzyskać pomoc od kapitana, który zna te wody . Starałem się jak najdokładniej śledzić rozmowy, na które patrzyłem, dla własnej korzyści i korzyści dla przyszłych czytelników.

Widziałem dużo atramentu rozlanego na zachowanie __getattribute__stosowane do deskryptorów, np. Pierwszeństwo wyszukiwania. Python snippet w „Wywoływanie Descriptors” tuż poniżej For classes, the machinery is in type.__getattribute__()...grubsza zgadza się w moim umyśle, co moim zdaniem jest odpowiednie źródło CPython w type_getattro, które śledzone przez patrząc na „tp_slots” wtedy gdzie tp_getattro jest zaludnionych . A fakt, że B.vpoczątkowo drukuje, __get__, obj=None, objtype=<class '__main__.B'>ma dla mnie sens.

Nie rozumiem tylko, dlaczego przypisanie B.v = 3ślepo zastępuje deskryptor, a nie wyzwala v.__set__? Próbowałem prześledzić wywołanie CPython, zaczynając od „tp_slots” , następnie sprawdzając, gdzie jest wypełniony tp_setattro , a potem patrząc na type_setattro . type_setattro wygląda na cienkie opakowanie wokół _PyObject_GenericSetAttrWithDict . I jest sedno mojego zamieszania: _PyObject_GenericSetAttrWithDictwydaje się, że ma logikę, która daje pierwszeństwo __set__metodzie deskryptora !! Mając to na uwadze, nie mogę zrozumieć, dlaczego B.v = 3ślepo zastępuje, va nie wyzwala v.__set__.

Zastrzeżenie 1: Nie przebudowałem Pythona ze źródła za pomocą printfs, więc nie jestem całkowicie pewien, type_setattrojak to się nazywa B.v = 3.

Zastrzeżenie 2: VocalDescriptornie ma na celu zilustrowania „typowej” lub „zalecanej” definicji deskryptora. Mówienie mi, kiedy metody są wywoływane, jest pełne.

Michael Carilli
źródło
1
Dla mnie to drukuje 3 w ostatnim wierszu ... Kod działa dobrze
Jab
3
Deskryptory mają zastosowanie podczas uzyskiwania dostępu do atrybutów z instancji , a nie z samej klasy. Dla mnie tajemnicą jest, dlaczego __get__w ogóle działało, a nie dlaczego __set__.
jasonharper
1
@Jab OP nadal będzie wywoływał tę __get__metodę. B.v = 3skutecznie zastąpił atrybut wartością int.
r.ook
2
@jasonharper Dostęp do atrybutu określa, czy __get__jest wywoływany, a także domyślne implementacje object.__getattribute__i type.__getattribute__wywołanie __get__przy użyciu instancji lub klasy. Przypisywanie za pośrednictwem __set__dotyczy tylko instancji.
chepner
@ jasonharper Uważam, że __get__metody deskryptorów powinny się uruchamiać po wywołaniu z samej klasy. W ten sposób zaimplementowano metody @classmethods i @staticmethods, zgodnie z instrukcją . @Jab Zastanawiam się, dlaczego B.v = 3można zastąpić deskryptor klasy. W oparciu o implementację CPython spodziewałem B.v = 3się również, że zadziała __set__.
Michael Carilli

Odpowiedzi:

6

Masz rację, że B.v = 3po prostu nadpisuje deskryptor liczbą całkowitą (tak jak powinno).

Aby B.v = 3wywołać deskryptor, deskryptor powinien być zdefiniowany na metaklasie, tj type(B). Na .

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

Aby wywołać deskryptor B, należy użyć instancji: B().v = 3zrobi to.

Powodem B.vwywołania gettera jest umożliwienie zwrócenia samej instancji deskryptora. Zwykle robiłbyś to, aby umożliwić dostęp do deskryptora za pośrednictwem obiektu klasy:

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

Teraz B.vzwróci jakieś wystąpienie, z <mymodule.VocalDescriptor object at 0xdeadbeef>którym możesz wchodzić w interakcje. Jest to dosłownie obiekt deskryptora, zdefiniowany jako atrybut klasy, a jego stan B.v.__dict__jest wspólny dla wszystkich instancji klasy B.

Oczywiście to od użytkownika zależy, B.vczy dokładnie zdefiniuje, co chce zrobić, zwracanie selfto tylko wspólny wzorzec.

wim
źródło
1
Aby uzupełnić tę odpowiedź, dodałbym, że __get__ma on być wywoływany jako atrybut instancji lub atrybut klasy, ale __set__został zaprojektowany tak, aby wywoływać tylko jako atrybut instancji. I odpowiednie dokumenty: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash
@wim Magnificent !! Równolegle patrzyłem jeszcze raz na łańcuch połączeń type_setattro. Widzę, że wywołanie _PyObject_GenericSetAttrWithDict zapewnia typ (w tym przypadku B, w moim przypadku).
Michael Carilli
Wewnątrz _PyObject_GenericSetAttrWithDict, to ciągnie Py_TYPE z B jako tp, która jest metaklasa B jest ( typew moim przypadku), to jest to metaklasa tp który jest traktowany przez logikę zwarciowego deskryptora . Tak więc deskryptor zdefiniowany bezpośrednio B nie jest widziany przez tę logikę zwarcia (dlatego w moim oryginalnym kodzie __set__nie jest wywoływany), ale deskryptor zdefiniowany na metaklasie jest postrzegany przez logikę zwarcia.
Michael Carilli
Dlatego w twoim przypadku, gdy metaklasa ma deskryptor, wywoływana jest__set__ metoda tego deskryptora .
Michael Carilli
@ sanyash możesz edytować bezpośrednio.
wim
3

Z wyłączeniem wszelkich przesłonięć, B.vjest równoważne type.__getattribute__(B, "v"), podczas gdy b = B(); b.vjest równoważne z object.__getattribute__(b, "v"). Obie definicje wywołują __get__metodę wyniku, jeśli została zdefiniowana.

Należy pamiętać, że wezwanie do __get__różni się w każdym przypadku. B.vprzechodzi Nonejako pierwszy argument, a B().vprzekazuje samą instancję. W obu przypadkach Bjest przekazywany jako drugi argument.

B.v = 3z drugiej strony jest równoważne z type.__setattr__(B, "v", 3), który nie wywołuje __set__.

chepner
źródło