Почему установка дескриптора для класса перезаписывает дескриптор?

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 фрагмент кода в «Вызов Дескрипторы» чуть ниже 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__вообще сработало , а не почему .
Джейсон Харпер
1
@Jab OP ожидает вызова __get__метода. B.v = 3эффективно переписал атрибут с int.
r.ook
2
@jasonharper Доступ к атрибутам определяет, __get__будет ли вызываться, и реализации по умолчанию object.__getattribute__и type.__getattribute__вызывать __get__при использовании экземпляра или класса. Назначение через __set__только для экземпляра.
chepner
@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- это просто общий шаблон.

Wim
источник
1
Чтобы завершить этот ответ, я бы добавил, что __get__он предназначен для вызова в качестве атрибута экземпляра или атрибута класса, но __set__предназначен для вызова только в качестве атрибута экземпляра. И соответствующие документы: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash
@ Вим Великолепно! Параллельно я снова посмотрел на цепочку вызовов type_setattro. Я вижу, что вызов _PyObject_GenericSetAttrWithDict предоставляет тип (в этой точке B, в моем случае).
Майкл Карилли
Внутри _PyObject_GenericSetAttrWithDictон извлекает Py_TYPE из B как 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__.

chepner
источник