Атрибуты и протокол дескриптора в Python
Вы, возможно, уже знаете, что у большинства объектов есть внутренний словарь __dict__, содержащий все их аттрибуты. И что особенно радует, как легко можно изучать такие низкоуровневые детали в Питоне:
Давайте начнём с попытки сформулировать такую (неполную) гипотезу:
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__
Хм… ну ладно. Видно что __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 "", line 1, in foo.baz File "", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ >
Пара вещей на заметку относительно этого кода:
- __setattr__ не имеет своего аналога __getattribute__ (т.е. магического метода __setattribute__ не существует).
- __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(, '__doc__': None, '__getattr__': , '__getattribute__': , '__module__': '__main__', '__weakref__': , 'bar': >)
Ключ 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 в целом корректна, учтите, что всегда могут найтись небольшие детали, реализованные по-другому.
Надеюсь, что помимо знаний о том, как работают атрибуты, мне так же удалось передать красоту языка, который поощряет вас к экспериментам. Погасите часть долга знаний сегодня.