📜 ⬆️ ⬇️

Decorator cached_property

How often do you write such constructions?

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 

This is very convenient, the value of the param attribute with this approach is not stored directly in the object, but it is not calculated every time. The calculation occurs on the first call, and this value is stored in the object under the temporary name _param. If the conditions on which the value of param depends, can be deleted, and then it will be calculated again at the next call. Or you can immediately assign the actual value, if known.

This code has its drawbacks: an object has an extra attribute named _param; each time the attribute is accessed, the param () method is called, which makes a hasattr check; the resulting code is quite large, especially if there are several attributes in the class.

You can get rid of the _param attribute by working directly with the object dictionary:
')
 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'] 

Here, the calculated value is stored in an attribute with the same name as the descriptor. Due to the fact that the @property decorator creates a data descriptor (the so-called descriptors with the declared __set __ () method), our getter and setter are executed even if the required attribute is present in the __dict__ object dictionary. And because we work with this __dict__ directly, bypassing the attributes of the object, there are no conflicts and endless recursions.

But in the code above, there are still too many common parts. The second attribute of the same will differ only in the function of computing (). Let's try to make a separate decorator who will do all the rough work. And you can use this decorator like this:

 class SomeClass(object): @cached_property def param(self): return computing() 

The rest of the code is transferred to the decorator itself:

 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] 

One could stop at this. But in Python it seems that, specifically for such cases, descriptors are divided into data descriptors and not data. A data descriptor should only have a __get __ () method, and when accessing an attribute, this method will not be called if there is already a value in the object dictionary. Those. we only need to remove the __set __ () and __delete __ () methods, as the Python interpreter will itself check for the existence of an attribute in the object's dictionary. As a result, the @cached_property decorator is simplified several times:

 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 

Such a decorator has long been used in many projects on Python and can be imported from django.utils.functional, starting with Django 1.4. Its use is so simple and cheap that it is worth using it in any place where you can postpone the calculation of some attributes. For example:

 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) 

You can remake on:

 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) 

Source: https://habr.com/ru/post/159099/


All Articles