Welcome, guest | Sign In | My Account | Store | Cart

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

Python, 12 lines
 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)

7 comments

Sridhar Ratnakumar (author) 12 years, 7 months ago  # | flag

Example,

class Foo(object):
    @lazyproperty
    def bar(self):
        # do some length operation
        value = calculate()
        return value

f = Foo()
for x in range(10):
    print f.bar
Alan Franzoni 12 years, 7 months ago  # | flag

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:

class Foo(object):
    attr1 = None
    attr2 = None

    @lazyproperty
    def bar(self, a, b, c):
        value = calculate(a, b, c, self.attr1, self.attr2)
        return value

I think a proper way to do this should take into account the Foo instance state BEYOND the input arguments.

Matteo Dell'Amico 12 years, 7 months ago  # | flag

Alan: perhaps a @mutating decorator to declare that some methods should reset one of more of the cached properties?

Alan Franzoni 12 years, 7 months ago  # | flag

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.

Sridhar Ratnakumar (author) 12 years, 7 months ago  # | flag

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.

Alan Franzoni 12 years, 7 months ago  # | flag

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):

class MyMutableObject(object):
    _counter = 0

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

        self.__class__._counter += 1
        self.counter = self._counter

    @lazyproperty
    def complex_property(self):
        return self.a + self.b

    def __getstate__(self):
        return (self.a, self.b, self.c)

    def __repr__(self):
        return "MMO %s: a=%s, b=%s, c=%s" % (self.counter, self.a, self.b,
                self.c)

mmo1 = MyMutableObject(5, 10, 12)
mmo2 = MyMutableObject(1, 2, 3)

print "First access"
print mmo1.complex_property
print mmo2.complex_property
print "Reaccess"
print mmo1.complex_property
print mmo2.complex_property

print "Change values and reaccess."
mmo1.a = 1
mmo1.b = 2
mmo1.c = 3

print mmo1.complex_property

Will output:

First access
15
3
Reaccess
15
3
Change values and reaccess.
15

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:

def statelazyproperty(func):
    """A decorator for state-based lazy evaluation of properties
    """
    cache = {}
    def _get(self):
        state = self.__getstate__()
        try:
            v = cache[state]
            print "Cache hit %s" % str(state)
            return v
        except KeyError:
            print "Cache miss %s" % str(state)
            cache[state] = value = func(self)
            return value

    return property(_get)

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.

Sridhar Ratnakumar (author) 12 years, 7 months ago  # | flag

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__.

Created by Sridhar Ratnakumar on Wed, 15 Apr 2009 (MIT)
Python recipes (4591)
Sridhar Ratnakumar's recipes (7)

Required Modules

  • (none specified)

Other Information and Tasks