Почему установка дескриптора для класса перезаписывает дескриптор? - PullRequest
9 голосов
/ 16 октября 2019

Простое воспроизведение:

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 не предназначен для иллюстрации «типичного» или «рекомендуемого» определения дескриптора. Это многословный запрет на опросы, когда вызываются методы.

Ответы [ 2 ]

6 голосов
/ 16 октября 2019

Вы правы, что 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 вызова геттераразрешить возврат самого экземпляра дескриптора. Обычно вы делаете это, чтобы разрешить доступ к дескриптору через объект класса:

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 - это просто общая схема.

3 голосов
/ 16 октября 2019

За исключением любых переопределений, 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__.

...