How can I delay the __init__ call until an attribute is accessed?

Viewed 4949

I have a test framework that requires test cases to be defined using the following class patterns:

class TestBase:
    def __init__(self, params):
        self.name = str(self.__class__)
        print('initializing test: {} with params: {}'.format(self.name, params))

class TestCase1(TestBase):
    def run(self):
        print('running test: ' + self.name)

When I create and run a test, I get the following:

>>> test1 = TestCase1('test 1 params')
initializing test: <class '__main__.TestCase1'> with params: test 1 params
>>> test1.run()
running test: <class '__main__.TestCase1'>

The test framework searches for and loads all TestCase classes it can find, instantiates each one, then calls the run method for each test.

load_test(TestCase1(test_params1))
load_test(TestCase2(test_params2))
...
load_test(TestCaseN(test_params3))

...

for test in loaded_tests:
    test.run()

However, I now have some test cases for which I don't want the __init__ method called until the time that the run method is called, but I have little control over the framework structure or methods. How can I delay the call to __init__ without redefining the __init__ or run methods?


Update

The speculations that this originated as an XY problem are correct. A coworker asked me this question a while back when I was maintaining said test framework. I inquired further about what he was really trying to achieve and we figured out a simpler workaround that didn't involve changing the framework or introducing metaclasses, etc.

However, I still think this is a question worth investigating: if I wanted to create new objects with "lazy" initialization ("lazy" as in lazy evaluation generators such as range, etc.) what would be the best way of accomplishing it? My best attempt so far is listed below, I'm interested in knowing if there's anything simpler or less verbose.

8 Answers

In my answer I'd like to focus on cases when one wants to instantiate a class whose initialiser (dunder init) has side effects. For instance, pysftp.Connection, creates an SSH connection, which may be undesired until it's actually used.

In a great blog series about conceiving of wrapt package (nit-picky decorator implementaion), the author describes Transparent object proxy. This code can be customised for the subject in question.

class LazyObject:

    _factory = None
    '''Callable responsible for creation of target object'''

    _object = None
    '''Target object created lazily'''

    def __init__(self, factory):
        self._factory = factory

    def __getattr__(self, name):
        if not self._object:
            self._object = self._factory()

        return getattr(self._object, name)

Then it can be used as:

obj = LazyObject(lambda: dict(foo = 'bar'))
obj.keys()  # dict_keys(['foo'])

But len(obj), obj['foo'] and other language constructs which invoke Python object protocols (dunder methods, like __len__ and __getitem__) will not work. However, for many cases, which are limited to regular methods, this is a solution.

To proxy object protocol implementations, it's possible to use neither __getattr__, nor __getattribute__ (to do it in a generic way). The latter's documentation notes:

This method may still be bypassed when looking up special methods as the result of implicit invocation via language syntax or built-in functions. See Special method lookup.

As a complete solution is demanded, there are examples of manual implementations like werkzeug's LocalProxy and django's SimpleLazyObject. However a clever workaround is possible.

Luckily there's a dedicated package (based on wrapt) for the exact use case, lazy-object-proxy which is described in this blog post.

from lazy_object_proxy import Proxy

obj = Proxy(labmda: dict(foo = 'bar'))
obj.keys()     # dict_keys(['foo'])
len(len(obj))  # 1
obj['foo']     # 'bar'
Related