为什么为 Python 函数中引发的异常创建变量名会影响该函数的输入变量的引用计数?

And*_*rew 4 python garbage-collection exception reference-counting

我定义了两个简单的 Python 函数,它们接受单个参数、引发异常并处理引发的异常。一个函数在引发/处理之前使用变量来引用异常,另一个函数则不使用:

def refcount_unchanged(x):
    try:
        raise Exception()
    except:
        pass

def refcount_increases(x):
    e = Exception()
    try:
        raise e
    except:
        pass
Run Code Online (Sandbox Code Playgroud)

结果函数之一增加了refcount其输入参数的 pythons,另一个则没有:

import sys

a = []
print(sys.getrefcount(a))
for i in range(3):
    refcount_unchanged(a)
    print(sys.getrefcount(a))
# prints: 2, 2, 2, 2

b = []
print(sys.getrefcount(b))
for i in range(3):
    refcount_increases(b)
    print(sys.getrefcount(b))
# prints: 2, 3, 4, 5
Run Code Online (Sandbox Code Playgroud)

谁能解释为什么会发生这种情况

aar*_*ron 5

这是PEP-344__traceback__ (Python 2.5)中引入的异常实例属性中的“异常 -> 回溯 -> 堆栈帧 -> 异常”引用循环的副作用,并在PEP-3110 (Python 3.0)等情况下解决)。refcount_unchanged

在 中refcount_increases,可以通过打印以下内容来观察引用循环:

except:
    print(e.__traceback__.tb_frame.f_locals)  # {'x': [], 'e': Exception()}
Run Code Online (Sandbox Code Playgroud)

这表明它x也在框架的局部变量中被引用。

当垃圾收集器运行或gc.collect()调用 if 时,引用循环将被解决。

在 中refcount_unchanged,根据 PEP-3110 的语义更改,Python 3 生成额外的字节码来删除目标,从而消除引用循环:

def refcount_unchanged(x):
    try:
        raise Exception()
    except:
        pass
Run Code Online (Sandbox Code Playgroud)

被翻译成这样:

def refcount_unchanged(x):
    try:
        raise Exception()
    except Exception as e:
        try:
            pass
        finally:
            e = None
            del e
Run Code Online (Sandbox Code Playgroud)

解决引用循环问题refcount_increases

虽然没有必要(因为垃圾收集器将完成其工作),但您可以refcount_increases通过手动删除变量引用来执行类似的操作:

def refcount_increases(x):
    e = Exception()
    try:
        raise e
    except:
        pass
    finally:   # +
        del e  # +
Run Code Online (Sandbox Code Playgroud)

或者,您可以覆盖变量引用并让隐式删除起作用:

def refcount_increases(x):
    e = Exception()
    try:
        raise e
    # except:               # -
    except Exception as e:  # +
        pass
Run Code Online (Sandbox Code Playgroud)

关于引用循环的更多信息

异常e和其他局部变量实际上是由 直接引用的e.__traceback__.tb_frame,大概是在 C 代码中。

这可以通过打印以下内容来观察:

print(sys.getrefcount(b))
print(gc.get_referrers(b)[0])  # <frame at ...>
Run Code Online (Sandbox Code Playgroud)

访问e.__traceback__.tb_frame.f_locals会创建一个缓存在帧上的字典(另一个引用周期),并阻碍上面的主动解决方案。

print(sys.getrefcount(b))
print(gc.get_referrers(b)[0])  # {'x': [], 'e': Exception()}
Run Code Online (Sandbox Code Playgroud)

然而,这个引用周期也将由垃圾收集器处理。