如果函数引发错误,则显式删除函数中的变量

Explicitly delete variables within a function if the function raised an error

提问人:Mathieu 提问时间:8/29/2023 最后编辑:Mathieu 更新时间:9/6/2023 访问量:31

问:

单元测试的一个特殊问题。我的许多测试函数具有以下结构:

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_variablestryexceptfinally

尝试,不工作:

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

由于不引用执行 中的命名空间。如果我使用 ,我不知道我应该提供什么参数,因为就 Python 而言,该命名空间已经是 GC。任何知道我如何让该装饰器工作的想法,即我如何从命名空间中显式删除变量?locals()functionvars()function


注意:是的,这是一个奇怪的情况,我需要显式调用,因为我覆盖了某些对象的方法(存储在我要删除的变量中)来调用 c++ 函数来销毁附加的 c++ 对象和引用。不,在这种情况下,pytest 夹具不是一个好的解决方案;)del__del__

Python 命名空间 pytest 装饰器

评论

0赞 Barmar 8/29/2023
您不需要删除局部变量,当您离开函数的作用域时,它们都会消失。
0赞 Mathieu 8/29/2023
@Barmar 阅读注释部分。这是一个边缘情况,我确实需要显式调用方法,或者一些 C++ 附加对象不会被销毁,即使删除了 python 引用,C++ 附加对象仍然存在并弄乱了其他测试(甚至破坏了测试隔离..)。del
0赞 Barmar 8/29/2023
我不认为你可以用装饰器做到这一点。装饰器环绕着一个函数,但你需要一些在函数中运行的东西。
0赞 Barmar 8/29/2023
我仍然认为你不需要使用.该方法在对象进行垃圾回收时运行。该语句强制立即发生这种情况(如果变量是对对象的唯一引用),但当函数退出时也会发生同样的事情。del__del__del
0赞 Mathieu 8/29/2023
@Barmar 可悲的是,显式删除作品,同时让它在函数“closure”上完成,失败了。所以这个丑陋的说法是有道理的。我想知道你说“装饰器环绕一个函数,但你需要一些在函数中运行的东西”,我完全同意。我现在正在寻找一种使用给定命名空间执行此函数的方法,或者创建命名空间并将该命名空间的引用提供给该函数,以便在执行后保持其活动状态。有什么想法吗?del

答:

1赞 danzel 9/6/2023 #1

你真的应该考虑重构你的代码。一开始就依赖终结器不是一个好主意(例如,请参阅此问答)。有更好的机制可用于资源清理,例如上下文管理器和/或pytest固定装置。

话虽如此:

如果测试失败,pytest 将保留引发的异常。该异常包含所有堆栈帧。测试函数的堆栈帧包含对测试中创建的所有对象的引用。

使用当前解决方案时,只需删除测试函数中的引用。但是,对其他堆栈帧中的对象可能有更多引用。例如:

def func(some_obj):
    raise Exception

def test_test():
    obj = MyObj()
    func(obj)

del obj不会导致对象被垃圾回收,因为 Now 的堆栈帧仍然包含对该对象的引用。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 现在也没有机会在其输出中告诉您局部变量的值。

评论

0赞 Mathieu 9/6/2023
谢谢,我从这个答案中学到了一些关于pytest的东西:)