Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 959
Billy Avatar asked Jul 19 '17 15:07

Billy


2 Answers

First Solution:use property.the elegant way of setter/getter in python.

class Bars(object):
    def __init__(self):
        self._foo = None

    @property
    def foo(self):
        if not self._foo:
            print("lazy initialization")
            self._foo =  [1,2,3]
        return self._foo

if __name__ == "__main__":
    f = Bars()
    print(f.foo)
    print(f.foo)

Second Solution:the proxy solution,and always implement by decorator.

In short, Proxy is a wrapper that wraps the object you need. Proxy could provide additional functionality to the object that it wraps and doesn't change the object's code. It's a surrogate which provide the abitity of control access to a object.there is the code come form user Cyclone.

class LazyProperty:
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        print('value {}'.format(value))
        setattr(obj, self.method_name, value)
        return value

class test:
    def __init__(self):
        self._resource = None

    @LazyProperty
    def resource(self):
        print("lazy")
        self._resource = tuple(range(5))
        return self._resource
if __name__ == '__main__':
    t = test()
    print(t.resource)
    print(t.resource)
    print(t.resource)

To be used for true one-time calculated lazy properties. I like it because it avoids sticking extra attributes on objects, and once activated does not waste time checking for attribute presence

like image 153
obgnaw Avatar answered Oct 13 '22 00:10

obgnaw


Metaclass option

You can intercept the call to __init__ using a metaclass. Create the object with __new__ and overwrite the __getattribute__ method to check if __init__ has been called or not and call it if it hasn't.

class DelayInit(type):

    def __call__(cls, *args, **kwargs):

        def init_before_get(obj, attr):
            if not object.__getattribute__(obj, '_initialized'):
                obj.__init__(*args, **kwargs)
                obj._initialized = True
            return object.__getattribute__(obj, attr)

        cls.__getattribute__ = init_before_get

        new_obj = cls.__new__(cls, *args, **kwargs)
        new_obj._initialized = False
        return new_obj

class TestDelayed(TestCase1, metaclass=DelayInit):
    pass

In the example below, you'll see that the init print won't occur until the run method is executed.

>>> new_test = TestDelayed('delayed test params')
>>> new_test.run()
initializing test: <class '__main__.TestDelayed'> with params: delayed test params
running test: <class '__main__.TestDelayed'>

Decorator option

You could also use a decorator that has a similar pattern to the metaclass above:

def delayinit(cls):

    def init_before_get(obj, attr):
        if not object.__getattribute__(obj, '_initialized'):
            obj.__init__(*obj._init_args, **obj._init_kwargs)
            obj._initialized = True
        return object.__getattribute__(obj, attr)

    cls.__getattribute__ = init_before_get

    def construct(*args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        obj._init_args = args
        obj._init_kwargs = kwargs
        obj._initialized = False
        return obj

    return construct

@delayinit
class TestDelayed(TestCase1):
    pass

This will behave identically to the example above.

like image 36
Billy Avatar answered Oct 13 '22 01:10

Billy