Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create decorator for lazy initialization of a property

I want to create a decorator that works like a property, only it calls the decorated function only once, and on subsequent calls always return the result of the first call. An example:

def SomeClass(object):
    @LazilyInitializedProperty
    def foo(self):
        print "Now initializing"
        return 5

>>> x = SomeClass()
>>> x.foo
Now initializing
5
>>> x.foo
5

My idea was to write a custom decorator for this. So i started, and this is how far I came:

class LazilyInitializedProperty(object):
    def __init__(self, function):
        self._function = function

    def __set__(self, obj, value):
        raise AttributeError("This property is read-only")

    def __get__(self, obj, type):
        # problem: where to store the value once we have calculated it?

As you can see, I do not know where to store the cached value. The simplest solution seems to be to just maintain a dictionary, but I am wondering if there is a more elegant solution for this.

EDIT Sorry for that, I forgot to mention that I want the property to be read-only.

like image 208
Björn Pollex Avatar asked Jul 13 '10 13:07

Björn Pollex


People also ask

How do you implement a public property using lazy initialization?

To implement a public property by using lazy initialization, define the backing field of the property as a Lazy<T>, and return the Value property from the get accessor of the property. The Value property is read-only; therefore, the property that exposes it has no set accessor.

What is lazy initialization of an object?

Lazy initialization of an object means that its creation is deferred until it is first used. (For this topic, the terms lazy initialization and lazy instantiation are synonymous.)

How does exception caching work with lazy initialized objects?

If a lazy-initialized object has exception caching enabled and throws an exception from its initialization method when the Value property is first accessed, that same exception is thrown on every subsequent attempt to access the Value property. In other words, the constructor of the wrapped type is never re-invoked, even in multithreaded scenarios.

How to create a lazy<T> object from a set accessor?

The set accessor must create a lambda expression that returns the new property value that was passed to the set accessor, and pass that lambda expression to the constructor for the new Lazy<T> object.


1 Answers

Denis Otkidach's CachedAttribute is a method decorator which makes attributes lazy (computed once, accessible many). To make it also read-only, I added a __set__ method. To retain the ability to recalculate (see below) I added a __delete__ method:

class ReadOnlyCachedAttribute(object):    
    '''Computes attribute value and caches it in the instance.
    Source: Python Cookbook 
    Author: Denis Otkidach https://stackoverflow.com/users/168352/denis-otkidach
    This decorator allows you to create a property which can be computed once and
    accessed many times. Sort of like memoization
    '''
    def __init__(self, method, name=None):
        self.method = method
        self.name = name or method.__name__
        self.__doc__ = method.__doc__
    def __get__(self, inst, cls): 
        if inst is None:
            return self
        elif self.name in inst.__dict__:
            return inst.__dict__[self.name]
        else:
            result = self.method(inst)
            inst.__dict__[self.name]=result
            return result    
    def __set__(self, inst, value):
        raise AttributeError("This property is read-only")
    def __delete__(self,inst):
        del inst.__dict__[self.name]

For example:

if __name__=='__main__':
    class Foo(object):
        @ReadOnlyCachedAttribute
        # @read_only_lazyprop
        def bar(self):
            print 'Calculating self.bar'  
            return 42
    foo=Foo()
    print(foo.bar)
    # Calculating self.bar
    # 42
    print(foo.bar)    
    # 42
    try:
        foo.bar=1
    except AttributeError as err:
        print(err)
        # This property is read-only
    del(foo.bar)
    print(foo.bar)
    # Calculating self.bar
    # 42

One of the beautiful things about CachedAttribute (and ReadOnlyCachedAttribute) is that if you del foo.bar, then the next time you access foo.bar, the value is re-calculated. (This magic is made possible by the fact that del foo.bar removes 'bar' from foo.__dict__ but the property bar remains in Foo.__dict__.)

If you don't need or don't want this ability to recalculate, then the following (based on Mike Boers' lazyprop) is a simpler way to make a read-only lazy property.

def read_only_lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__
    @property
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)
    @_lazyprop.setter
    def _lazyprop(self,value):
        raise AttributeError("This property is read-only")
    return _lazyprop
like image 55
unutbu Avatar answered Oct 21 '22 08:10

unutbu