Question or problem about Python programming:
Recently I’ve gone through an existing code base containing many classes where instance attributes reflect values stored in a database. I’ve refactored a lot of these attributes to have their database lookups be deferred, ie. not be initialised in the constructor but only upon first read. These attributes do not change over the lifetime of the instance, but they’re a real bottleneck to calculate that first time and only really accessed for special cases. Hence they can also be cached after they’ve been retrieved from the database (this therefore fits the definition of memoisation where the input is simply “no input”).
I find myself typing the following snippet of code over and over again for various attributes across various classes:
class testA(object): def __init__(self): self._a = None self._b = None @property def a(self): if self._a is None: # Calculate the attribute now self._a = 7 return self._a @property def b(self): #etc
Is there an existing decorator to do this already in Python that I’m simply unaware of? Or, is there a reasonably simple way to define a decorator that does this?
I’m working under Python 2.5, but 2.6 answers might still be interesting if they are significantly different.
This question was asked before Python included a lot of ready-made decorators for this. I have updated it only to correct terminology.
How to solve the problem:
Solution 1:
For all sorts of great utilities I’m using boltons.
As part of that library you have cachedproperty:
from boltons.cacheutils import cachedproperty class Foo(object): def __init__(self): self.value = 4 @cachedproperty def cached_prop(self): self.value += 1 return self.value f = Foo() print(f.value) # initial value print(f.cached_prop) # cached property is calculated f.value = 1 print(f.cached_prop) # same value for the cached property - it isn't calculated again print(f.value) # the backing value is different (it's essentially unrelated value)
Solution 2:
Here is an example implementation of a lazy property decorator:
import functools def lazyprop(fn): attr_name = '_lazy_' + fn.__name__ @property @functools.wraps(fn) def _lazyprop(self): if not hasattr(self, attr_name): setattr(self, attr_name, fn(self)) return getattr(self, attr_name) return _lazyprop class Test(object): @lazyprop def a(self): print 'generating "a"' return range(5)
Interactive session:
>>> t = Test() >>> t.__dict__ {} >>> t.a generating "a" [0, 1, 2, 3, 4] >>> t.__dict__ {'_lazy_a': [0, 1, 2, 3, 4]} >>> t.a [0, 1, 2, 3, 4]
Solution 3:
I wrote this one for myself… To be used for true one-time calculated lazy properties. I like it because it avoids sticking extra attributes on objects, and once activated does not waste time checking for attribute presence, etc.:
import functools class lazy_property(object): ''' meant to be used for lazy evaluation of an object attribute. property should represent non-mutable data, as it replaces itself. ''' def __init__(self, fget): self.fget = fget # copy the getter function's docstring and other attributes functools.update_wrapper(self, fget) def __get__(self, obj, cls): if obj is None: return self value = self.fget(obj) setattr(obj, self.fget.__name__, value) return value class Test(object): @lazy_property def results(self): calcs = 1 # Do a lot of calculation here return calcs
Note: The lazy_property
class is a non-data descriptor, which means it is read-only. Adding a __set__
method would prevent it from working correctly.
Solution 4:
Here’s a callable that takes an optional timeout argument, in the __call__
you could also copy over the __name__
, __doc__
, __module__
from func’s namespace:
import time class Lazyproperty(object): def __init__(self, timeout=None): self.timeout = timeout self._cache = {} def __call__(self, func): self.func = func return self def __get__(self, obj, objcls): if obj not in self._cache or \ (self.timeout and time.time() - self._cache[key][1] > self.timeout): self._cache[obj] = (self.func(obj), time.time()) return self._cache[obj]
ex:
class Foo(object): @Lazyproperty(10) def bar(self): print('calculating') return 'bar' >>> x = Foo() >>> print(x.bar) calculating bar >>> print(x.bar) bar ...(waiting 10 seconds)... >>> print(x.bar) calculating bar
Solution 5:
property
is a class. A descriptor to be exact. Simply derive from it and implement the desired behavior.
class lazyproperty(property): .... class testA(object): .... a = lazyproperty('_a') b = lazyproperty('_b')