Атрибуты и протокол дескриптора в Python

от автора

Рассмотрим такой код:

class Foo:     def __init__(self):         self.bar = 'hello!'  foo = Foo() print(foo.bar)

Сегодня мы разберём ответ на вопрос: «Что именно происходит, когда мы пишем foo.bar


Вы, возможно, уже знаете, что у большинства объектов есть внутренний словарь __dict__, содержащий все их аттрибуты. И что особенно радует, как легко можно изучать такие низкоуровневые детали в Питоне:

>>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'}

Давайте начнём с попытки сформулировать такую (неполную) гипотезу:

foo.bar эквивалентно foo.__dict__[‘bar’] .

Пока звучит похоже на правду:

>>> foo = Foo() >>> foo.__dict__['bar'] 'hello!'

Теперь предположим, что вы уже в курсе, что в классах можно объявлять динамические аттрибуты:

>>> class Foo: ...     def __init__(self): ...         self.bar = 'hello!' ...          ...     def __getattr__(self, item): ...         return 'goodbye!' ...          ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'}

Хм… ну ладно. Видно что __getattr__  может эмулировать доступ к «ненастоящим» атрибутам, но не будет работать, если уже есть объявленная переменная (такая, как foo.bar, возвращающая ‘hello!’, а не ‘goodbye!’). Похоже, всё немного сложнее, чем казалось вначале.

И действительно: существует магический метод, который вызывается всякий раз, когда мы пытаемся получить атрибут, но, как продемонстрировал пример выше, это не __getattr__. Вызываемый метод называется __getattribute__, и мы попробуем понять, как в точности он работает, наблюдая различные ситуации.

Пока что модифицируем нашу гипотезу так:

foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:

def __getattribute__(self, item):   if item in self.__dict__:     return self.__dict__[item]   return self.__getattr__(item)

Проверим практикой, реализовав этот метод (под другим именем) и вызывая его напрямую:

>>> class Foo: ...     def __init__(self): ...         self.bar = 'hello!' ...          ...     def __getattr__(self, item): ...         return 'goodbye!' ...      ...     def my_getattribute(self, item): ...         if item in self.__dict__: ...             return self.__dict__[item] ...         return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!'

Выглядит корректно, верно?

Отлично, осталось лишь проверить, что поддерживается присвоение переменных, после чего можно расходиться по дом… —

>>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call

Чёрт.

my_getattribute возвращает некий объект. Мы можем изменить его, если он мутабелен, но мы не можем заменить его на другой с помощью оператора присвоения. Что же делать? Ведь если foo.baz это эквивалент вызова функции, как мы можем присвоить новое значение атрибуту в принципе?

Когда мы смотрим на выражение типа foo.bar = 1, происходит что-то больше, чем просто вызов функции для получения значения foo.bar. Похоже, что присвоение значения атрибуту фундаментально отличается от получения значения атрибута. И правда: мы может реализовать __setattr__, чтобы убедиться в этом:

>>> class Foo: ...     def __init__(self): ...         self.__dict__['my_dunder_dict'] = {} ...         self.bar = 'hello!' ...          ...     def __setattr__(self, item, value): ...         self.my_dunder_dict[item] = value ...      ...     def __getattr__(self, item): ...         return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last):   File "<pyshell#75>", line 1, in <module>     foo.baz   File "<pyshell#70>", line 10, in __getattr__     return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}}

Пара вещей на заметку относительно этого кода:

  1. __setattr__  не имеет своего аналога __getattribute__  (т.е. магического метода __setattribute__ не существует).
  2. __setattr__  вызывается внутри __init__, именно поэтому мы  вынуждены делать self.__dict__[‘my_dunder_dict’] = {} вместо self.my_dunder_dict = {}. В противном случае мы столкнулись бы с бесконечной рекурсией.

А ведь у нас есть ещё и property (и его друзья). Декоратор, который позволяет методам выступать в роли атрибутов.

Давайте постараемся понять, как это происходит.

>>> class Foo(object): ...     def __getattribute__(self, item): ...         print('__getattribute__ was called') ...         return super().__getattribute__(item) ...      ...     def __getattr__(self, item): ...         print('__getattr__ was called') ...         return super().__getattr__(item) ...      ...     @property ...     def bar(self): ...          print('bar property was called') ...          return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called

Просто ради интереса, а что у нас в f.__dict__?

>>> f.__dict__ __getattribute__ was called {}

В __dict__ нет ключа bar, но __getattr__  почему-то не вызывается. WAT?

bar — метод, да ещё и принимающий в качестве параметра self, вот только это метод находится в классе, а не в экземпляре класса. И в этом легко убедиться:

>>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>,               '__doc__': None,               '__getattr__': <function Foo.__getattr__ at 0x038308A0>,               '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>,               '__module__': '__main__',               '__weakref__': <attribute '__weakref__' of 'Foo' objects>,               'bar': <property object at 0x0381EC30>})

Ключ bar действительно находится в словаре атрибутов класса. Чтобы понять работу __getattribute__, нам нужно ответить на вопрос: чей __getattribute__  вызывается раньше — класса или экземпляра?

>>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100

Видно, что первым делом проверка идёт в __dict__  класса, т.е. у него приоритет перед экземпляром.

Погодите-ка, а когда мы вызывали метод bar? Я имею в виду, что наш псевдокод для __getattribute__  никогда не вызывает объект. Что же происходит?

Встречайте протокол дескриптора:

descr.__get__(self, obj, type=None) -> value 

descr.__set__(self, obj, value) -> None 

descr.__delete__(self, obj) -> None 

Вся суть тут. Реализуйте любой из этих трёх методов, чтобы объект стал дескриптором и мог менять дефолтное поведение, когда с ним работают как с атрибутом.

Если объект объявляет и __get__(), и __set__(), то его называют дескриптором данных («data descriptors»). Дескрипторы реализующие лишь __get__() называются дескрипторами без данных («non-data descriptors»).

Оба вида дескрипторов отличаются тем, как происходит перезапись элементов словаря атрибутов объекта. Если словарь содержит ключ с тем же именем, что и у дескриптора данных, то дескриптор данных имеет приоритет (т.е. вызывается __set__()). Если словарь содержит ключ с тем же именем, что у дескриптора без данных, то приоритет имеет словарь (т.е. перезаписывается элемент словаря).

Чтобы создать дескриптор данных доступный только для чтения, объявите и __get__(), и __set__(), где __set__() кидает AttributeError при вызове. Реализации такого __set__() достаточно для создания дескриптора данных.

Короче говоря, если вы объявили любой из этих методов — __get____set__  или __delete__, вы реализовали поддержку протокола дескриптора. А это именно то, чем занимается декоратор property: он объявляет доступный только для чтения дескриптор, который будет вызываться в __getattribute__.

Последнее изменение нашей реализации:

foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:

def __getattribute__(self, item):   if item in self.__class__.__dict__:     v = self.__class__.__dict__[item]   elif item in self.__dict__:     v = self.__dict__[item]   else:     v = self.__getattr__(item)   if hasattr(v, '__get__'):     v = v.__get__(self, type(self))   return v

Попробуем продемонстрировать на практике:

class Foo:     class_attr = "I'm a class attribute!"          def __init__(self):         self.dict_attr = "I'm in a dict!"              @property     def property_attr(self):         return "I'm a read-only property!"          def __getattr__(self, item):         return "I'm dynamically returned!"          def my_getattribute(self, item):       if item in self.__class__.__dict__:         print('Retrieving from self.__class__.__dict__')         v = self.__class__.__dict__[item]       elif item in self.__dict__:         print('Retrieving from self.__dict__')         v = self.__dict__[item]       else:         print('Retrieving from self.__getattr__')         v = self.__getattr__(item)       if hasattr(v, '__get__'):         print("Invoking descriptor's __get__")         v = v.__get__(self, type(self))       return v
>>> foo = Foo() ...  ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ...  ... print() ...  ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned!  Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned!

Мы лишь немного поскребли поверхность реализации атрибутов в Python. Хотя наша последняя попытка эмулировать foo.bar в целом корректна, учтите, что всегда могут найтись небольшие детали, реализованные по-другому.

Надеюсь, что помимо знаний о том, как работают атрибуты, мне так же удалось передать красоту языка, который поощряет вас к экспериментам. Погасите часть долга знаний сегодня.


ссылка на оригинал статьи https://habr.com/ru/post/479824/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *