How to create lazy initialization (proxy) wrapper in python?

125 views Asked by At

Given an expensive to initialize object and 2 consumers:

class A:
  def __init__():
    time.sleep(42)
    self.foo = 1

def func1(obj: A):
  pass

def func2(obj: A):
  print(A.foo)

How to create a wrapper that would delay initialization of A until any of its fields are accessed?

proxy = wrapper(A)
func1(proxy) # executes immediately
func2(proxy) # causes object initialization and waits 42 seconds.

In other words, how to delay object initialization until any of its properties are accessed in a conventional way a.foo or at worst a.__dict__['foo'].

1

There are 1 answers

0
Alain T. On

You could write a general purpose decorator for this. Assuming you add a function called __lazyinit__ to your class, this decorator will ensure it is called before each of the decorated method that require the full initialization of the object.

def lazyinit(f):
    def doInit(self,*args,**kwargs):
        self.__lazyinit__()
        return f(self,*args,**kwargs)
    return doInit


class A:

    def __init__(self,p1,p2):
        self._p1 = p1
        self._p2 = p2
        self._initComplete = False

    def __lazyinit__(self):
        if self._initComplete: return
        self._p3 = self._p1 + self._p2
        self._initComplete = True

    @lazyinit                  # works for functions
    def funcX(self):
        print(self._p3)

    @property
    @lazyinit                  # also works for properties
    def p3(self):
        return self._p3

Examples:

a = A(2,3)
a._p3        # Attribute Error
a.funcX()    # prints 5

a2 = A(4,5)
a2._p3       # Attribute Error
print(a2.p3) # prints 9

Note that this isn't much different from adding a call to self.__lazyinit__() at the beginning of every function but it does place that concern/aspect outside of the function's code

If you're only using this for properties you could place the @property logic inside of your decorator to form a @lazyproperty decorator that combines the two

Alternatively ...

If you don't want to make any changes to the original class, you could write a function that dynamically creates a wrapper class for your object instance. The function would return the un-initialized object and let the __getattr__ initialize the object when a reference is made to an attribute that is not already present (i.e. because the instance is not initialized).

def lazy(objClass, *args, **kwargs):

    class LazyClass(objClass):
        def __getattr__(self,name):
            self.__init__(*args,**kwargs)
            return getattr(self,name)
        
    return objClass.__new__(LazyClass)            
    
class A:

    def __init__(self,p1,p2):
        print('initing A')
        self._p1 = p1
        self._p2 = p2
        self._p3 = p1 + p2

a = lazy(A,2,3)
print("instantiated A") # 'initing A' not yet printed

print(a._p3)
# initing A
# 5
print(a._p3)            
# 5