单元测试的一个特殊问题。我的许多测试函数都具有以下结构:
def test_xxx():
try:
# do-something
variable1 = ...
variable2 = ...
except Exception as error:
raise error
finally:
try:
del variable1
except Exception:
pass
try:
del variable2
except Exception:
pass
而且这个结构显然不是很好。我可以用以下方式简化
finally
语句:
finally:
for variable in ("variable1", "variable2"):
if variable in locals():
del locals()[variable]
但它仍然不是那么好。相反,我想使用装饰器
del_variables
来处理 try
/ except
/ finally
。
尝试,不起作用:
def del_variables(*variables):
def decorator(function):
def wrapper(*args, **kwargs):
try:
return function(*args, **kwargs)
except Exception as error:
raise error
finally:
for variable in variables:
if variable in locals():
del locals()[variable]
return wrapper
return decorator
@del_variables("c")
def foo(a, b):
c = 3
return a + b + c
因为
locals()
并不引用 function
执行中的命名空间。如果我使用 vars()
,我不知道应该提供什么参数,因为就 Python 而言,该命名空间已经是 GC 了。知道如何让装饰器工作吗,即如何从 function
的命名空间中显式删除变量?
注意:是的,这是一个奇怪的情况,我需要显式调用
del
,因为我覆盖了某些对象(存储在我想要删除的变量中)的__del__
方法来调用c++函数来销毁c++对象并附参考文献。不,在这种情况下,pytest 装置并不是一个好的解决方案;)
你真的应该考虑重构你的代码。一开始就依赖终结器并不是一个好主意(例如参见此问答)。有更好的机制可用于资源清理,例如上下文管理器和/或 pytest 固定装置。
话虽这么说:
如果测试失败,pytest 会保留引发的异常。该异常包含所有堆栈帧。测试函数的堆栈帧包含对测试中创建的所有对象的引用。
使用当前的解决方案,您只需删除测试函数中的引用。然而,可能有更多对其他堆栈帧中的对象的引用。例如:
def func(some_obj):
raise Exception
def test_test():
obj = MyObj()
func(obj)
del obj
不会导致对象被垃圾回收,因为现在func
的堆栈帧仍然包含对该对象的引用。
由于您无法通过调用堆栈合理地跟踪对对象的引用(它可能包含在其他对象等内部的字典中),因此我认为没有简单的方法可以仅删除您请求的选定对象。但是,假设没有对对象的全局或循环引用,您可以简单地从后续帧中删除所有局部变量。哦,当然,我们只讨论 CPython。
这可以通过多种方式实现,但因为你想要一个装饰器:
import ctypes
import sys
def delete_locals(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
finally:
tb = sys.exc_info()[2]
if tb:
while tb.tb_next:
tb = tb.tb_next
tb.tb_frame.f_locals.clear()
ctypes.pythonapi.PyFrame_LocalsToFast(
ctypes.py_object(tb.tb_frame), ctypes.c_int(1)
) # reference: https://stackoverflow.com/a/34671307
return wrapper
请记住,pytest 现在也没有机会告诉您输出中局部变量的值。