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


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

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问题 是正确的。一段时间,当我维护测试框架时,一位同事问我这个问题。我进一步询问他是什么  试图实现,我们想出了一个更简单的解决方法,不涉及更改框架或引入元类等。

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


9219
2017-07-19 15:09


起源

是否只是延迟了一些与之相关的繁重初始化 self.name? - Ashwini Chaudhary
对于某些情况,是的。 - Billy
然后更好地使他们的属性,并在他们的吸气剂做繁重的工作。听起来像是一个 XY问题 马上。 - Ashwini Chaudhary
如果没有你禁止的改动就没有办法实现你的要求,只要更深入地了解你的要求,我想有人可能会帮助你。 - holdenweb
你可以添加一个超类或元类 TestBase? - Jared Goguen


答案:


第一解决方案使用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是一个包装你需要的对象的包装器。代理可以为它包装的对象提供附加功能,而不会更改对象的代码。它是一个代理,提供对对象的控制访问的权限。代码来自形式 用户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)

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


9
2017-07-27 03:28





元类选项

你可以拦截来电 __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

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

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

这将与上面的示例相同。


3
2017-07-19 15:11



阅读你的问题,但你为什么不这样做 run 你的初始化方法,这是懒惰的?我有一些方法执行非常昂贵的操作(例如网络访问)。所有你需要做的就是使一个实例方法变得懒惰,一旦它完成它的工作,你只需要用非惰性方法替换它自己的方法。也许我会很快就我对我的程序所做的回答。 - Zizouz212


在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 实例。


1
2017-07-28 10:56





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

在一个关于构思的伟大博客系列中 wrapt 作者描述了包(nit-picky decorator implementaion) 透明对象代理。可以针对相关主题自定义此代码。

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__ (以通用方式完成)。后者的文件 笔记

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

由于需要完整的解决方案,因此有一些手动实现的例子 WERKZEUGLocalProxy 和 Django的SimpleLazyObject。然而,一个聪明的解决方法是 可能

幸运的是,有一个专门的包(基于 包起)对于确切的用例, 懒对象代理 其中描述了 这篇博文

from lazy_object_proxy import Proxy

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

1
2018-01-25 13:19





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

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

0
2017-07-19 16:07





回答你的原始问题(以及我认为你实际上试图解决的问题),“我怎么能推迟 在里面 打电话直到访问属性?“:不要打电话 在里面 直到您访问该属性。

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

可能你的原始问题来自于认为你必须初始化所有你的问题 TestCase类,以便创建可以迭代的列表。但实际上你可以存储类对象 listsdicts 这意味着你可以采取任何方法来找到所有 TestCase 类和存储这些类对象 dict 与他们的相关参数。然后重复一遍 dict 并用它来调用每个类 run() 方法。

它可能看起来像:

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

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

0
2017-07-26 19:22





Overridding __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_property 是一个 非数据描述符。因此,一旦在实例的字典中设置了密钥,对属性的访问将始终从那里获取值。

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)

0
2017-07-28 12:00





我认为你可以使用包装类来保存你想要实例的真实类,并使用call __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__")

但这是一种倾销方式,但没有任何诡计。


0
2017-07-31 01:38