Чому встановлення дескриптора на клас замінює дескриптор?


10

Просте запитання:

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

На це питання є ефективний дублікат , але на дублікат не було відповіді, і я трохи більше заглибився у джерело CPython як навчальну вправу. Попередження: я зайшов у бур’яни. Я дуже сподіваюся, що зможу отримати допомогу від капітана, який знає ці води . Я намагався бути максимально чітким у відстеженні дзвінків, на які я дивився, задля власної майбутньої вигоди та користі майбутніх читачів.

Я бачив багато чорнила, що пролилося на поведінку __getattribute__застосованих до дескрипторів, наприклад, пріоритет пошуку. Фрагмент Python у "Invoking Descriptors" трохи нижче For classes, the machinery is in type.__getattribute__()...приблизно відповідає моїй думці з тим, що, на мою думку, є відповідним джерелом CPython, в type_getattroякому я відстежив, дивлячись на "tp_slots", а потім там, де tp_getattro заселений . А те, що B.vспочатку друкує, __get__, obj=None, objtype=<class '__main__.B'>має сенс для мене.

Що я не розумію, це те, чому завдання B.v = 3сліпо переписує дескриптор, а не спрацьовує v.__set__? Я спробував простежити виклик CPython, починаючи ще раз з "tp_slots" , потім дивлячись, де tp_setattro заселений , потім дивлячись на type_setattro . type_setattro виявляється тонкою обгорткою навколо _PyObject_GenericSetAttrWithDict . І в цьому суть моєї плутанини: _PyObject_GenericSetAttrWithDictсхоже, є логіка, яка надає перевагу дескрипторному __set__методу !! Зважаючи на це, я не можу зрозуміти, чому B.v = 3сліпо переписує, vа не викликає v.__set__.

Застереження 1: Я не відновлював Python з джерела з printfs, тому я не зовсім впевнений type_setattro, що називається під час B.v = 3.

Відмова від відповідальності 2: VocalDescriptorне призначений для прикладу визначення "типового" або "рекомендованого" дескриптора. Це багатослівний не-оп, щоб сказати мені, коли методи викликаються.


1
Для мене це друкує 3 в останньому рядку ... Код працює добре
Jab

3
Дескриптори застосовуються під час доступу до атрибутів із екземпляра , а не самого класу. Для мене загадка полягає в тому __get__, чому __set__взагалі працювали, а не чому ні.
jasonharper

1
@Jab OP розраховує все-таки використати __get__метод. B.v = 3ефективно перезаписав атрибут на int.
r.ook

2
Доступ до атрибутів @jasonharper визначає, чи __get__викликається, а також реалізація за замовчуванням object.__getattribute__та type.__getattribute__виклик __get__при використанні екземпляра чи класу. Призначення через __set__лише екземпляр.
чепнер

@jasonharper Я вважаю, що __get__методи дескрипторів повинні спрацьовувати, коли викликаються з самого класу. Ось як реалізуються @classmethods та @staticmethods відповідно до інструкції . @Jab Мені цікаво, чому B.v = 3можна перезаписати дескриптор класу. На основі реалізації CPython, я очікував, що B.v = 3він також спрацьовує __set__.
Майкл Каріллі

Відповіді:


6

Ви праві, що B.v = 3дескриптор просто перезаписується цілим числом (як слід).

Щоб B.v = 3викликати дескриптор, дескриптор повинен був бути визначений у метакласі, тобто на type(B).

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

Щоб викликати дескриптор увімкнено B , ви використовуєте екземпляр: B().v = 3зробимо це.

Причина B.vвиклику getter полягає в тому, щоб дозволити повернення самого екземпляра дескриптора. Зазвичай ви робите це, щоб дозволити доступ до дескриптора через об’єкт класу:

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

Тепер B.vповернеться якийсь екземпляр, з <mymodule.VocalDescriptor object at 0xdeadbeef>яким ви можете взаємодіяти. Це буквально об’єкт дескриптора, визначений як атрибут класу, так і його станB.v.__dict__ поділяється між усіма примірниками B.

Звичайно, саме за кодом користувача потрібно точно визначити, що він хоче B.vробити, повернення self- це лише загальна модель.


1
Для завершення цієї відповіді я додам, що __get__призначений для виклику як атрибута екземпляра або атрибута класу, але __set__призначений для виклику лише як атрибута екземпляра. І відповідні документи: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash

@Wim Чудовий !! Паралельно я ще раз переглядав ланцюжок викликів type_setattro. Я бачу, що виклик до _PyObject_GenericSetAttrWithDict надає тип (у моєму випадку B).
Майкл Каріллі

Всередині _PyObject_GenericSetAttrWithDictвоно витягує Py_TYPE B as tp, який є метакласом B ( typeу моєму випадку), то це метаклас tp обробляється дескриптовою логікою короткого замикання . Отже, дескриптор, визначений безпосередньо на B , не бачиться цією логікою короткого замикання (тому в моєму вихідному коді __set__не називається), але дескриптор, визначений у метакласі , бачиться логікою короткого замикання.
Майкл Каріллі

Таким чином, у вашому випадку , коли метаклассом має дескриптор, то __set__метод цього дескриптора є називаються.
Майкл Каріллі

@sanyash не соромтесь безпосередньо редагувати.
Вім

3

Заборона будь-яких переопределень B.vеквівалентна type.__getattribute__(B, "v"), а b = B(); b.vеквівалентна object.__getattribute__(b, "v"). Обидва визначення посилаються на__get__ метод результату, якщо він визначений.

Зауважте, подумав, що виклик __get__відрізняється у кожному випадку. B.vпередає Noneяк перший аргумент, тоді як B().vпередає сам екземпляр. В обох випадкахB передається як другий аргумент.

B.v = 3з іншого боку, еквівалентний тому type.__setattr__(B, "v", 3), що не викликає __set__.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.