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
feed
Comments
5 comments postedclass Printer(object):
def __get__(self, obj, klass):
if not hasattr(self, '_cached'):
print('calculate...')
self._cached = 'cached'
return self._cached
class MyClass(object):
cached_val = Printer()
# Rest of your code here
I can't see a need to combine a descriptor and a decorator like this... Or am I missing something?
Sorry about the formatting.
Ben
It might be me that is missing something here, but I think what the usage of decorator allows is multiple cached properties without a custom class for each property. For example:
>>> a = A('world') >>> b = A('moon') >>> a.hello [computing hello] 'Hello, world' >>> b.hello 'Hello, world' >>>Post new comment