Python class member lazy initialization
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_DATAclass 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_datacostly 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_propertyclass 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_cacheclass 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:
@memoizedef expensive_function(): print "Computing expensive function..." import time time.sleep(1) return 400print expensive_function()print expensive_function()print expensive_function()
Which outputs:
Computing expensive function...400400400
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_dataprint MyClass.retrieve_data()print MyClass.retrieve_data()print MyClass.retrieve_data()
Output:
Computing data404040
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.