我有一个测试框架,需要使用以下类模式定义测试用例:
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
等),那将是什么完成它的最好方法是什么?到目前为止我的最佳尝试如下所示,我有兴趣知道是否有更简单或更简洁的事情。
答案 0 :(得分:10)
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
类,以便创建可以迭代的列表。但事实上,您可以在lists
,dicts
等中存储类对象。这意味着您可以使用任何方法查找所有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_property
是non-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__")
但它是一种转储方式,但没有任何技巧。