Cached_property decorator

    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 param attribute value in 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 param value depends change, you can delete it, and then it will be calculated again at the next call. Or you can immediately assign the current value, if one is known.

    This code has its drawbacks: the object has an extra attribute named _param; every time the attribute is called, the param () method is called, which does hasattr; the resulting code is large enough, especially if there are several such 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 descriptors with the declared method __set __ () are called), our getters and setters are executed even if the required attribute is present in the __dict__ object dictionary. And due to the fact that we work with this __dict__ directly, bypassing the attributes of the object, there are no conflicts and endless recursions.

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

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

    The rest of the code is transferred to the decorator descriptor 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 there. But in Python, as if specially for such cases, the descriptors are divided into data and non-data descriptors. The non-data descriptor should have only the __get __ () method, and when accessing the attribute, this method will not be called if the object's dictionary already has a value. Those. if we remove the __set __ () and __delete __ () methods, the Python interpreter will itself check for the existence of the 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
    

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

    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)
    

    Can be converted to:

    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)
    

    Also popular now: