Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python class member lazy initialization

People also ask

What is lazy initialization in Python?

In computer programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed. It is a kind of lazy evaluation that refers specifically to the instantiation of objects or other resources.

Is lazy initialization good practice?

Lazy initialization is an excellent performance optimization technique, allowing you to defer the initialization of objects that consume significant CPU and memory resources until you absolutely need them. Take advantage of lazy initialization to improve the performance of your apps.

What is LazyValue?

Lazy values are useful for defaults that are expensive to construct or are seldom retrieved. The first time a LazyValue is retrieved its "real value" is computed by calling LazyValue. createValue() and the real value is used to replace the LazyValue in the UIDefaults table.

Is lazy initialization a design pattern?

The lazy initialization pattern embodies the just-in-time philosophy of data delivery. It delays the construction of values or data structures until that data is actually needed. Lazy initialization is a popular design pattern in both Java and Objective-C.


You could use a @property on the metaclass instead:

class MyMetaClass(type):
    @property
    def my_data(cls):
        if getattr(cls, '_MY_DATA', None) is None:
            my_data = ...  # costly database call
            cls._MY_DATA = my_data
        return cls._MY_DATA


class MyClass(metaclass=MyMetaClass):
    # ...

This makes my_data an attribute on the class, so the expensive database call is postponed until you try to access MyClass.my_data. The result of the database call is cached by storing it in MyClass._MY_DATA, the call is only made once for the class.

For Python 2, use class MyClass(object): and add a __metaclass__ = MyMetaClass attribute in the class definition body to attach the metaclass.

Demo:

>>> class MyMetaClass(type):
...     @property
...     def my_data(cls):
...         if getattr(cls, '_MY_DATA', None) is None:
...             print("costly database call executing")
...             my_data = 'bar'
...             cls._MY_DATA = my_data
...         return cls._MY_DATA
... 
>>> class MyClass(metaclass=MyMetaClass):
...     pass
... 
>>> MyClass.my_data
costly database call executing
'bar'
>>> MyClass.my_data
'bar'

This works because a data descriptor like property is looked up on the parent type of an object; for classes that's type, and type can be extended by using metaclasses.


This answer is for a typical instance attribute/method only, not for a class attribute/classmethod, or staticmethod.

For Python 3.8+, how about using the cached_property decorator? It memoizes.

from functools import cached_property

class MyClass:

    @cached_property
    def my_lazy_attr(self):
        print("Initializing and caching attribute, once per class instance.")
        return 7**7**8

For Python 3.2+, how about using both property and lru_cache decorators? The latter memoizes.

from functools import lru_cache

class MyClass:

    @property
    @lru_cache()
    def my_lazy_attr(self):
        print("Initializing and caching attribute, once per class instance.")
        return 7**7**8

Credit: answer by Maxime R.


Another approach to make the code cleaner is to write a wrapper function that does the desired logic:

def memoize(f):
    def wrapped(*args, **kwargs):
        if hasattr(wrapped, '_cached_val'):
            return wrapped._cached_val
        result = f(*args, **kwargs)
        wrapped._cached_val = result
        return result
    return wrapped

You can use it as follows:

@memoize
def expensive_function():
    print "Computing expensive function..."
    import time
    time.sleep(1)
    return 400

print expensive_function()
print expensive_function()
print expensive_function()

Which outputs:

Computing expensive function...
400
400
400

Now your classmethod would look as follows, for example:

class MyClass(object):
        @classmethod
        @memoize
        def retrieve_data(cls):
            print "Computing data"
            import time
            time.sleep(1) #costly DB call
            my_data = 40
            return my_data

print MyClass.retrieve_data()
print MyClass.retrieve_data()
print MyClass.retrieve_data()

Output:

Computing data
40
40
40

Note that this will cache just one value for any set of arguments to the function, so if you want to compute different values depending on input values, you'll have to make memoize a bit more complicated.