如何在访问属性之前延迟__init__调用?

时间:2017-07-19 15:09:19

标签: python python-3.x

我有一个测试框架,需要使用以下类模式定义测试用例:

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)

当我创建并运行测试时,我得到以下内容:

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

测试框架搜索并加载它可以找到的所有TestCase类,实例化每个类,然后为每个测试调用run方法。

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

...

for test in loaded_tests:
    test.run()

但是,我现在有一些测试用例,在调用__init__方法之前我不希望调用run方法,但是我几乎无法控制框架结构或方法。如何在不重新定义__init____init__方法的情况下延迟拨打run

更新

这种起源为XY problem的推测是正确的。一段时间,当我维护测试框架时,一位同事问我这个问题。我进一步询问他实际试图实现什么,我们想出了一个更简单的解决方法,不涉及更改框架或引入元类等。

但是,我仍然认为这是一个值得研究的问题:如果我想创建具有“懒惰”初始化的新对象(如惰性评估生成器中的“懒惰”,如range等),那将是什么完成它的最好方法是什么?到目前为止我的最佳尝试如下所示,我有兴趣知道是否有更简单或更简洁的事情。

8 个答案:

答案 0 :(得分:10)

第一个解决方案:使用property.python中setter / getter的优雅方式。

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)

第二个解决方案:代理解决方案,并始终由装饰者实施。

简而言之,Proxy是一个包装你需要的对象的包装器。代理可以为它包装的对象提供附加功能,并且不会更改对象的代码。它是一个代理,提供对对象的控制访问权限。代码来自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)
  

用于真正的一次性计算延迟属性。我喜欢它,因为它避免在对象上粘贴额外的属性,并且一旦激活就不会浪费时间检查属性存在

答案 1 :(得分:4)

元类选项

您可以使用元类拦截对__init__的调用。使用__new__创建对象并覆盖__getattribute__方法以检查是否已调用__init__,如果没有,则调用它。

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

在下面的示例中,您将看到在执行run方法之前不会发生init打印。

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

装饰器选项

您还可以使用与上面的元类具有相似模式的装饰器:

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

这与上面的例子相同。

答案 2 :(得分:1)

在Python中,当您实例化类__init__时,无法避免调用cls。如果调用cls(args)返回cls的实例,那么该语言可以保证cls.__init__将被调用。

因此,实现类似于你所要求的东西的唯一方法是引入另一个类,它将推迟原始类中__init__的调用,直到访问实例化类的属性为止。

这是一种方式:

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

此包装函数通过捕获任何访问实例化类的属性的尝试来工作。进行此类尝试时,它会将实例的__class__更改为原始类,使用创建实例时使用的参数调用原始__init__方法,然后返回正确的属性。此函数可用作TestCase1类的装饰器:

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

请注意应用此功能的位置。如果您使用TestBase修饰delay_init,它将无效,因为它会将TestCase1个实例转换为TestBase个实例。

答案 3 :(得分:1)

在我的回答中,我想关注一个人想要实例化一个初始化(dunder init)有副作用的类的情况。例如,pysftp.Connection会创建一个SSH连接,在实际使用之前可能不需要它。

在一个关于构思wrapt包(挑剔的装饰器实现)的伟大博客系列中,作者描述了Transparent object proxy。此代码可以针对相关主题进行自定义。

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)

然后它可以用作:

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

但是len(obj)obj['foo']和其他调用Python对象协议的语言结构(dunder方法,如__len____getitem__)将不起作用。但是,对于许多仅限于常规方法的情况,这是一种解决方案。

要代理对象协议实现,可以既不使用__getattr__,也不使用__getattribute__(以通用方式执行)。后者的文档notes

  

当通过语言语法或内置函数进行隐式调用查找特殊方法时,仍可以绕过此方法。见Special method lookup

作为一个完整的解决方案,有一些手动实现的例子,如 werkzeug LocalProxy django SimpleLazyObject。然而,一个聪明的解决方法是possible

幸运的是,有一个专用的包(基于 wrapt )用于lazy-object-proxy中描述的确切用例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'

答案 4 :(得分:0)

另一种方法是编写一个包装器,它将类作为输入,并返回一个具有延迟初始化的类,直到访问任何成员。例如,这可以这样做:

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

然后可以这样使用

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()

答案 5 :(得分:0)

回答您的原始问题(以及我认为您实际上试图解决的问题),&#34;如何在访问属性之前延迟 init 调用?&#34;:在您访问该属性之前,请不要调用 init

另一种说法:您可以使用属性调用同时进行类初始化。你似乎真正想要的是1)创建一个TestCase#类的集合及其相关参数; 2)运行每个测试用例。

可能你的原始问题来自于认为必须初始化所有TestCase类,以便创建可以迭代的列表。但事实上,您可以在listsdicts等中存储类对象。这意味着您可以使用任何方法查找所有TestCase类并将这些类对象存储在{{1与他们的相关参数。然后只需迭代dict并使用dict方法调用每个类。

看起来像是:

run()

答案 6 :(得分:0)

重叠__new__

您可以通过覆盖__new__方法并使用自定义函数替换__init__方法来完成此操作。

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

<强>演示:

>>> 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)

使用缓存属性

这个想法是通过缓存结果只进行一次繁重的计算。如果延迟初始化的整个过程都在提高性能,那么这种方法将会产生更易读的代码。

Django带来了一个名为@cached_property的漂亮装饰器。我倾向于在代码和单元测试中大量使用它来缓存重属性的结果。

cached_propertynon-data descriptor。因此,一旦在实例的字典中设置了密钥,对属性的访问将始终从那里获取值。

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

<强>用法:

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))

<强>演示:

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

答案 7 :(得分:0)

我认为您可以使用包装器类来保存您想要实例化的实际类,并在您的代码中自己使用调用__init__,如(Python 3代码):

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__")

但它是一种转储方式,但没有任何技巧。