问题 如何在访问属性之前延迟__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
起源
答案:
第一解决方案使用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
在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__
(以通用方式完成)。后者的文件 笔记:
当通过语言语法或内置函数进行隐式调用查找特殊方法时,仍可以绕过此方法。看到 特殊方法查找。
由于需要完整的解决方案,因此有一些手动实现的例子 WERKZEUG的 LocalProxy
和 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
类,以便创建可以迭代的列表。但实际上你可以存储类对象 lists
, dicts
这意味着你可以采取任何方法来找到所有 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