Python函数在异常期间的局部变量作用域

3 投票
3 回答
2438 浏览
提问于 2025-04-15 17:58

背景:我正在用Python进行国家仪器公司的TestStand的COM编程。如果对象没有被“释放”好,TestStand会弹出一个“对象没有被正确释放”的调试对话框。在Python中释放TestStand的COM对象的方法是确保所有变量不再包含这个对象,比如用del()删除它们,或者把它们设置为None。或者,只要这些变量是函数内部的局部变量,当函数结束时,变量超出作用域,对象就会被释放。

我在我的程序中遵循了这个规则,只要没有异常,我的程序就能正确释放对象。但如果出现异常,我就会收到TestStand的“对象没有被释放”的消息。这似乎表明,当发生异常时,函数的局部变量没有正常超出作用域。

这里有一个简化的代码示例:

class TestObject(object):
    def __init__(self, name):
        self.name = name
        print("Init " + self.name)
    def __del__(self):
        print("Del " + self.name)

def test_func(parameter):
    local_variable = parameter
    try:
        pass
#        raise Exception("Test exception")
    finally:
        pass
#        local_variable = None
#        parameter = None

outer_object = TestObject('outer_object')
try:
    inner_object = TestObject('inner_object')
    try:
        test_func(inner_object)
    finally:
        inner_object = None
finally:
    outer_object = None

当这个代码按预期运行时,结果是我所期待的:

Init outer_object
Init inner_object
Del inner_object
Del outer_object

但是如果我取消注释raise Exception...这一行,我得到的结果是:

Init outer_object
Init inner_object
Del outer_object
Traceback (most recent call last):
...
Exception: Test exception
Del inner_object

由于异常,inner_object的删除被延迟了。

如果我取消注释将parameterlocal_variable都设置为None的那几行,那么我就得到了我所期待的结果:

Init outer_object
Init inner_object
Del inner_object
Del outer_object
Traceback (most recent call last):
...
Exception: Test exception

所以,当Python中发生异常时,局部变量到底发生了什么?它们是不是被保存在某个地方,以至于没有像正常情况那样超出作用域?控制这种行为的“正确方法”是什么?

3 个回答

0

一个函数的作用范围是整个函数内部。你可以在finally块中处理这个问题。

1

根据这个回答,在处理异常时,可以通过检查异常追踪中的局部变量,方法是使用tb_frame.f_locals。所以看起来在处理异常的过程中,这些对象是会一直“活着”的。

2

你的异常处理可能因为保持对帧的引用而导致引用循环。正如文档中所说:

注意 保持对帧对象的引用,比如在这些函数返回的帧记录的第一个元素中,可能会导致你的程序产生引用循环。一旦形成了引用循环,从形成循环的对象可以访问的所有对象的生命周期可能会变得很长,即使Python的可选循环检测器是开启的。如果必须创建这样的循环,确保它们被明确打破是很重要的,以避免对象的延迟销毁和增加的内存消耗。虽然循环检测器会捕捉到这些,但通过在finally语句中移除循环,可以让帧(和局部变量)的销毁变得可预测。如果在编译Python时禁用了循环检测器,或者使用了gc.disable(),这点也很重要。例如:

def handle_stackframe_without_leak():
    frame = inspect.currentframe()
    try:
        # do something with the frame
    finally:
        del frame

撰写回答