Python does not have lazy evaluation syntax features built-in, but fortunately decorators can be used with new-style classes to emulate such a feature. There are cases where one wants foo.property
to return the actual property whose calculation takes significant amount of time.
This recipe adapts the existing property
to provide a lazypropery
decorator that does this.
See the first comment below for an example usage.
Also see: lazy initialization
1 2 3 4 5 6 7 8 9 10 11 12 | def lazyproperty(func):
"""A decorator for lazy evaluation of properties
"""
cache = {}
def _get(self):
try:
return cache[self]
except KeyError:
cache[self] = value = func(self)
return value
return property(_get)
|
Example,
The memoize pattern is pretty useful in order to cache heavy operations, but I think this kind of usage could be misleading. When used on a pure or almost-pure function, it could be assumed that the global state has little impact on the result.
But what if Foo class instances are not immutable, and the bar() method result does not depend just on the input value but on any of the object's attributes? E.g. something like this:
I think a proper way to do this should take into account the Foo instance state BEYOND the input arguments.
Alan: perhaps a @mutating decorator to declare that some methods should reset one of more of the cached properties?
Matteo: that might, or might not, be useful. If such changes happen frequently you just happen to reset your cache too often - if attr1 switches back and forth between 1 and 2, you just keep resetting the cache instead of caching two different values.
And it would be overly complex, IMHO, since a forgotten decorator might just make an app unpredictable.
Alan,
The motivation behind using lazy properties (for me) is to defer execution of the initialization code that sets member variables which is typically done in
__init__()
. The reason for deferring execution is that, one may want to avoid performing expensive calculation (in arriving at the values for member variables) during the object creation phase - and have that done only during the invocation of the member variable.Since this is a lazy property and not a lazy method, I would expect
Foo.bar()
to not accept any arguments; even if it did.. there is no way the dot accesor syntax -f.bar
- is able to pass the arguments to bar(). The argument is thus moot.I'll shortly update the recipe (by removing the memoize decorator) to avoid this confusion.
Sridhar,
I probably went wrong with my example.
But my point was a bit different; the problem is that any property might access other instance attributes. Whatever way they are created, those may vary during execution. Hence you should either clearly state this recipe only works on immutable objects, or find out a different caching algorithm.
Here's a "proper" example of the issue. Try this (there may be too much code here, but this will help with my next proposal):
Will output:
The last line's result might come just unexpected, because the object attributes have been changed, but no cache invalidation pattern has been applied. What I mean is: this sort of approach might be both error-prone and sub-effective: if such complex property results depends only on the object state, it won't probably vary between instances sharing an identical state; but this kind of lazyproperty forces a recalc every time a different instance is created.
This is my proposal:
Just let any instance have a __getstate__() method, and cache values upon that key. This makes the cache change-aware and lets it hit even though different instances get called.
Alan,
I did understand your original comment. As I said earlier, "The motivation ... is to defer execution of the initialization code".
See 'lazy initialization'.
BTW, in your example using
__hash__
is better than__getstate__
.