Billy
Billy

Reputation: 5609

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.

Upvotes: 17

Views: 7109

Answers (8)

saaj
saaj

Reputation: 25263

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'

Upvotes: 2

gansteed
gansteed

Reputation: 75

I think you can use a wrapper class to hold the real class you want to instance, and use call __init__ yourself in your code, like(Python 3 code):

class Wrapper:
    def __init__(self, cls):
        self.cls = cls
        self.instance = None

    def your_method(self, *args, **kwargs):
        if not self.instance:
            self.instnace = cls()
        return self.instance(*args, **kwargs)

class YourClass:
    def __init__(self):
        print("calling __init__")

but it's a dump way, but without any trick.

Upvotes: 0

Ashwini Chaudhary
Ashwini Chaudhary

Reputation: 251096

Overridding __new__

You could do this by overriding __new__ method and replacing __init__ method with a custom function.

def init(cls, real_init):
    def wrapped(self, *args, **kwargs):
        # This will run during the first call to `__init__`
        # made after `__new__`. Here we re-assign the original
        # __init__ back to class and assign a custom function
        # to `instances.__init__`.
        cls.__init__ = real_init
        def new_init():
            if new_init.called is False:
                real_init(self, *args, **kwargs)
                new_init.called = True
        new_init.called = False
        self.__init__ = new_init
    return wrapped


class DelayInitMixin(object):
    def __new__(cls, *args, **kwargs):
        cls.__init__ = init(cls, cls.__init__)
        return object.__new__(cls)


class A(DelayInitMixin):
    def __init__(self, a, b):
        print('inside __init__')
        self.a = sum(a)
        self.b = sum(b)

    def __getattribute__(self, attr):
        init = object.__getattribute__(self, '__init__')
        if not init.called:
            init()
        return object.__getattribute__(self, attr)

    def run(self):
        pass

    def fun(self):
        pass

Demo:

>>> a = A(range(1000), range(10000))    
>>> a.run()
inside __init__    
>>> a.a, a.b
(499500, 49995000)    
>>> a.run(), a.__init__()
(None, None)    
>>> b = A(range(100), range(10000))    
>>> b.a, b.b
inside __init__
(4950, 49995000)    
>>> b.run(), b.__init__()
(None, None)

Using cached properties

The idea is to do the heavy calculation only once by caching results. This approach will lead to much more readable code if the whole point of delaying initialization is improving performance.

Django comes with a nice decorator called @cached_property. I tend to use it a lot in both code and unit-tests for caching results of heavy properties.

A cached_property is a non-data descriptor. Hence once the key is set in instance's dictionary, the access to property would always get the value from there.

class cached_property(object):
    """
    Decorator that converts a method with a single self argument into a
    property cached on the instance.

    Optional ``name`` argument allows you to make cached properties of other
    methods. (e.g.  url = cached_property(get_absolute_url, name='url') )
    """
    def __init__(self, func, name=None):
        self.func = func
        self.__doc__ = getattr(func, '__doc__')
        self.name = name or func.__name__

    def __get__(self, instance, cls=None):
        if instance is None:
            return self
        res = instance.__dict__[self.name] = self.func(instance)
        return res

Usage:

class A:
    @cached_property
    def a(self):
        print('calculating a')
        return sum(range(1000))

    @cached_property
    def b(self):
        print('calculating b')
        return sum(range(10000))

Demo:

>>> a = A()
>>> a.a
calculating a
499500
>>> a.b
calculating b
49995000
>>> a.a, a.b
(499500, 49995000)

Upvotes: 0

Ruud de Jong
Ruud de Jong

Reputation: 784

In Python, there is no way that you can avoid calling __init__ when you instantiate a class cls. If calling cls(args) returns an instance of cls, then the language guarantees that cls.__init__ will have been called.

So the only way to achieve something similar to what you are asking is to introduce another class that will postpone the calling of __init__ in the original class until an attribute of the instantiated class is being accessed.

Here is one way:

def delay_init(cls):
    class Delay(cls):
        def __init__(self, *arg, **kwarg):
            self._arg = arg
            self._kwarg = kwarg
        def __getattribute__(self, name):
            self.__class__ = cls
            arg = self._arg
            kwarg = self._kwarg
            del self._arg
            del self._kwarg
            self.__init__(*arg, **kwarg)
            return getattr(self, name)
    return Delay

This wrapper function works by catching any attempt to access an attribute of the instantiated class. When such an attempt is made, it changes the instance's __class__ to the original class, calls the original __init__ method with the arguments that were used when the instance was created, and then returns the proper attribute. This function can be used as decorator for your TestCase1 class:

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)


>>> t1 = TestCase1("No delay")
initializing test: <class '__main__.TestCase1'> with params: No delay
>>> t2 = delay_init(TestCase1)("Delayed init")
>>> t1.run()
running test: <class '__main__.TestCase1'>
>>> t2.run()
initializing test: <class '__main__.TestCase1'> with params: Delayed init
running test: <class '__main__.TestCase1'>
>>> 

Be careful where you apply this function though. If you decorate TestBase with delay_init, it will not work, because it will turn the TestCase1 instances into TestBase instances.

Upvotes: 2

obgnaw
obgnaw

Reputation: 3037

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

Upvotes: 13

jack6e
jack6e

Reputation: 1522

Answering your original question (and the problem I think you are actually trying to solve), "How can I delay the init call until an attribute is accessed?": don't call init until you access the attribute.

Said another way: you can make the class initialization simultaneous with the attribute call. What you seem to actually want is 1) create a collection of TestCase# classes along with their associated parameters; 2) run each test case.

Probably your original problem came from thinking you had to initialize all your TestCase classes in order to create a list of them that you could iterate over. But in fact you can store class objects in lists, dicts etc. That means you can do whatever method you have for finding all TestCase classes and store those class objects in a dict with their relevant parameters. Then just iterate that dict and call each class with its run() method.

It might look like:

tests = {TestCase1: 'test 1 params', TestCase2: 'test 2 params', TestCase3: 'test 3 params'}

for test_case, param in tests.items():
    test_case(param).run()

Upvotes: 0

Billy
Billy

Reputation: 5609

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.

Upvotes: 9

Jonas Adler
Jonas Adler

Reputation: 10789

One alternative would be to write a wrapper that takes a class as input and returns a class with delayed initialization until any member is accessed. This could for example be done as this:

def lazy_init(cls):
    class LazyInit(cls):
        def __init__(self, *args, **kwargs):
            self.args = args
            self.kwargs = kwargs
            self._initialized = False

        def __getattr__(self, attr):
            if not self.__dict__['_initialized']:
                cls.__init__(self,
                             *self.__dict__['args'], **self.__dict__['kwargs'])
                self._initialized = True

            return self.__dict__[attr]

    return LazyInit

This could then be used as such

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

...

for test in loaded_tests:
    test.run()

Upvotes: 1

Related Questions