Декоратор cached_property

от автора

Как часто вы пишете такие конструкции?

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-%s-%s'     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-%s-%s'     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(page_num, per_page) 

ссылка на оригинал статьи http://habrahabr.ru/post/159099/


Комментарии

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

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