Lazy-Loading Cached Properties Using Descriptors and Decorators

Python descriptors allow for rather powerful and flexible attribute management with new-style classes. Combined with decorators, they make for some elegant programming. One useful application of these mechanisms are lazy-loading properties, i.e., properties with values that are computed only when first called, returning cached values on subsequent calls. An implementation of this concept (based on this post) is:

class lazy_property(object):
    """
    Lazy-loading read-only property descriptor.
    Value is computed and stored in owner class object's dictionary on first
    access. Subsequent calls use value in owner class object's dictionary
    directly.
    """

    def __init__(self, func):
        self._func = func
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj, obj_class):
        if obj is None:
            return obj
        obj.__dict__[self.__name__] = self._func(obj)
        return obj.__dict__[self.__name__]

Example usage:

class A(object):

    def __init__(self, s):
        self.s = s

    @lazy_property
    def hello(self):
        print("[computing hello]")
        return "Hello, %s" % self.s
>>> a = A('world')
>>> print(a.hello)
[computing hello]
Hello, world
>>> print(a.hello)
Hello, world
>>> print(a.hello)
Hello, world

Here, when the “hello” property of an object of class “A” is called for the first time, “lazy_property.__get__” calls on function “A.hello()” and stores the return value directly in dictionary of the calling object. Subsequent calls to the “hello” property of an object find this value in the object’s dictionary, and so both “lazy_property.__get__” and “A.hello()” are by-passed altogther. This is a neat way to handle values that are expensive to compute and are not always needed. However, one constraint of this approach is that it is not simple to force a re-computation or updating of the value. In addition, sometimes it would be nice to directly populate the property if required. What is needed for the latter case is a lazy-loading property that allows for setting as well as accessing. Furthermore, to support the former case, setting the property to “None” (or calling “del” on it) should force recomputation of its value on the next access. An implementation of this concept is [Updated on 2010-03-21 2013 CST, thanks to comment by anyonymous]:

class cached_property(object):
    """
    Lazy-loading read/write property descriptor.
    Value is stored locally in descriptor object. If value is not set when
    accessed, value is computed using given function. Value can be cleared
    by calling 'del'.
    """

    def __init__(self, func):
        self._func = func
        self._values =
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj, obj_class):
        if obj is None:
            return obj
        if obj not in self._values \
                or self._values[obj] is None:
            self._values[obj] = self._func(obj)
        return self._values[obj]

    def __set__(self, obj, value):
        self._values[obj] = value

    def __delete__(self, obj):
        if self.__name__ in obj.__dict__:
            del obj.__dict__[self.__name__]
        self._values[obj] = None

In action:

class A(object):

    def __init__(self, s):
        self.s = s

    @cached_property
    def hello(self):
        print("[computing hello]")
        return "Hello, %s" % self.s
>>> a = A('world')
>>> print(a.hello)
[computing hello]
Hello, world
>>> print(a.hello)
Hello, world
>>> del a.hello
>>> print(a.hello)
[computing hello]
Hello, world
>>> print(a.hello)
Hello, world
>>> a.hello = "Blah"
>>> print(a.hello)
Blah
>>> print(a.hello)
Blah
>>> del a.hello
>>> print(a.hello)
[computing hello]
Hello, world
>>> print(a.hello)
Hello, world
>>> a.hello = None
>>> print(a.hello)
[computing hello]
Hello, world
>>> print(a.hello)
Hello, world
Share