0

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'].

2
  • 1
    Have you tried anything so far? Seems like you just want something like functools.partial (for any initial arguments) plus __getattr__ (to intercept attribute access). See e.g. stackoverflow.com/q/26091833/3001761 for most of it. Commented Sep 21, 2023 at 19:09
  • I am trying to figure out what are proxy approaches in python overall. Move init to the overload of __getattr__ seems like a strong option, I just prefer to avoid __getattr__ for as long as possible since it brakes things. Additional source of complexity here is getting isinstance(wrapper(A), A) to work. Commented Sep 21, 2023 at 21:15

1 Answer 1

1

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
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.