Как часто вы пишете такие конструкции?
Это очень удобно, значение атрибута param при таком подходе не хранится напрямую в объекте, но и не вычисляется каждый раз. Вычисление происходит при первом обращении, и это значение сохраняется в объекте под временным именем _param. Если меняются условия, от которых зависит значение param, его можно удалить, и тогда оно снова вычислится при следующем обращении. Или можно сразу присвоить актуальное значение, если таковое известно.
У этого кода есть и минусы: у объекта появляется лишний атрибут с именем _param; при каждом обращении к атрибуту вызывается метод param(), который делает проверку hasattr; получившийся код достаточно большой, особенно если таких атрибутов в классе несколько.
Можно избавиться от атрибута _param, работая напрямую со словарем объекта:
Здесь вычисленное значение хранится в атрибуте с тем же именем, что и дескриптор. Из-за того, что декоратор @property создает дескриптор данных (так называются дескрипторы с объявленным методом __set__()), наши геттер и сеттер выполняются даже при наличии искомого атрибута в словаре объекта __dict__. А из-за того, что мы работаем с этим __dict__ напрямую, минуя атрибуты объекта, не происходит конфликтов и бесконечных рекурсий.
Но в приведенном коде по-прежнему слишком много общих частей. У второго такого же атрибута будет отличаться только функция computing(). Давайте попробуем сделать отдельный декоратор, который будет делать всю черновую работу. А использовать такой декоратор можно будет так:
В сам декоратор-дескриптор переносится весь остальной код:
Можно было бы на этом остановиться. Но в Питоне как будто специально для таких случаев дескрипторы делятся на дескрипторы данных и не данных. Дескриптор не данных должен иметь только метод __get__(), и при обращении к атрибуту этот метод не будет вызван, если в словаре объекта уже будет значение. Т.е. стоит нам убрать методы __set__() и __delete__(), как интерпретатор Питона будет сам делать проверку на существование атрибута в словаре объекта. В результате декоратор @cached_property упрощается в несколько раз:
Такой декоратор уже давно используется во многих проектах на Питоне и может быть импортирован из django.utils.functional, начиная с версии Джанго 1.4. Его использование настолько простое и дешевое, что стоит использовать его в любом месте, где можно отложить вычисление каких-то атрибутов. Например:
Можно переделать на:
class SomeClass(object): @property def param(self): if not hasattr(self, '_param'): self._param = computing() return self._param @param.setter def param(self, value): self._param = value @param.deleter def param(self): del self._param
Это очень удобно, значение атрибута param при таком подходе не хранится напрямую в объекте, но и не вычисляется каждый раз. Вычисление происходит при первом обращении, и это значение сохраняется в объекте под временным именем _param. Если меняются условия, от которых зависит значение param, его можно удалить, и тогда оно снова вычислится при следующем обращении. Или можно сразу присвоить актуальное значение, если таковое известно.
У этого кода есть и минусы: у объекта появляется лишний атрибут с именем _param; при каждом обращении к атрибуту вызывается метод param(), который делает проверку hasattr; получившийся код достаточно большой, особенно если таких атрибутов в классе несколько.
Можно избавиться от атрибута _param, работая напрямую со словарем объекта:
class SomeClass(object): @property def param(self): if 'param' not in self.__dict__: self.__dict__['param'] = computing() return self.__dict__['param'] @param.setter def param(self, value): self.__dict__['param'] = value @param.deleter def param(self): del self.__dict__['param']
Здесь вычисленное значение хранится в атрибуте с тем же именем, что и дескриптор. Из-за того, что декоратор @property создает дескриптор данных (так называются дескрипторы с объявленным методом __set__()), наши геттер и сеттер выполняются даже при наличии искомого атрибута в словаре объекта __dict__. А из-за того, что мы работаем с этим __dict__ напрямую, минуя атрибуты объекта, не происходит конфликтов и бесконечных рекурсий.
Но в приведенном коде по-прежнему слишком много общих частей. У второго такого же атрибута будет отличаться только функция computing(). Давайте попробуем сделать отдельный декоратор, который будет делать всю черновую работу. А использовать такой декоратор можно будет так:
class SomeClass(object): @cached_property def param(self): return computing()
В сам декоратор-дескриптор переносится весь остальной код:
class cached_property(object): def __init__(self, func): self.func = func self.name = func.__name__ def __get__(self, instance, cls=None): if self.name not in instance.__dict__: result = instance.__dict__[self.name] = self.func(instance) return result return instance.__dict__[self.name] def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name]
Можно было бы на этом остановиться. Но в Питоне как будто специально для таких случаев дескрипторы делятся на дескрипторы данных и не данных. Дескриптор не данных должен иметь только метод __get__(), и при обращении к атрибуту этот метод не будет вызван, если в словаре объекта уже будет значение. Т.е. стоит нам убрать методы __set__() и __delete__(), как интерпретатор Питона будет сам делать проверку на существование атрибута в словаре объекта. В результате декоратор @cached_property упрощается в несколько раз:
class cached_property(object): def __init__(self, func): self.func = func def __get__(self, instance, cls=None): result = instance.__dict__[self.func.__name__] = self.func(instance) return result
Такой декоратор уже давно используется во многих проектах на Питоне и может быть импортирован из django.utils.functional, начиная с версии Джанго 1.4. Его использование настолько простое и дешевое, что стоит использовать его в любом месте, где можно отложить вычисление каких-то атрибутов. Например:
class SomeList(object): storage_pattern = 'some-list-by-pages-{}-{}' def __init__(self, page_num, per_page): self.page_num, self.per_page = page_num, per_page self.storage_key = self.storage_pattern.format(page_num, per_page)
Можно переделать на:
class SomeList(object): storage_pattern = 'some-list-by-pages-{}-{}' def __init__(self, page_num, per_page): self.page_num, self.per_page = page_num, per_page @cached_property def storage_key(self): return self.storage_pattern.format(self.page_num, self.per_page)