nonlocals()在哪里?

8 投票
1 回答
1339 浏览
提问于 2025-04-17 10:52

我该如何获取当前作用域中的非本地变量?虽然有 varslocalsglobals 这些函数,但有没有什么函数可以获取 nonlocals 呢?

为什么调用 vars 时不显示非本地变量呢?

更新

我的问题是,当前作用域中没有办法列出可用的变量,因为我所知道的,varsglobals 都不包括非本地变量。

我经常在以下代码中使用 vars

'{meh[0]}/{meh[3]} {a}{b}{c}'.format(**vars())

如果这些变量在包含函数的作用域内,这段代码就会失败。

1 个回答

12

在运行的代码中,你可以很容易地获取非局部变量的名字,但要像调用 locals 那样获取它们的内容,就有点复杂了。

使用的 nonlocal 变量名存储在当前运行的代码对象中,具体在 co_freevars 属性里。

所以,获取非局部变量的名字可以这样做:

names = inspect.currentframe().f_code.co_freevars

不过,这些变量的 内容 是存储在 __closure__ 属性中(在 Python 2 中是 func_closure),而不是在代码对象里。问题是,没有“外部的帮助”,运行中的代码很难直接获取它所运行的函数对象。你可以获取到帧对象,它链接到代码对象,但没有指向函数对象的链接。(对于顶层定义的函数,可以直接使用在 def 语句中定义的函数名,但对于嵌套函数,返回给调用者时也无法知道它的名字)。

因此,必须使用一个小技巧——通过使用 gc 模块(垃圾回收器)来获取所有链接到当前代码对象的对象——可以调用 gc.get_referrers,它会返回所有链接到你持有的代码对象的函数对象。

所以,在一个有非局部变量的函数内部,你可以这样做:

import inspect, gc

from types import FunctionType

def a(b):
    b1 = 2
    def c():
        nonlocal b1
        print (b)
        code =  inspect.currentframe().f_code  
        names = code.co_freevars
        function = [func for func in gc.get_referrers(code) if isinstance(func, FunctionType)][0]
        nonlocals = dict (zip(names, (x.cell_contents for x in function.__closure__ )))
        print(nonlocals)
        return inspect.currentframe()
    return c

c = a(5)
f = c()

这样就可以获取非局部变量的名字和值了。但是 如果你有多个该函数的实例,这个方法就不管用了(也就是说,如果你多次调用生成该函数的函数,创建了多个实例)——因为所有这些实例都会链接到 同一个 代码对象。上面的例子假设当前代码只有一个函数在运行,在这种情况下是正确的。再次调用工厂函数会创建另一个函数,可能有不同的非局部变量值,但代码对象是相同的——上面的 function = 列表生成器会获取所有这些,并随意选择第一个。

“正确”的函数是当前代码正在执行的那个函数——我在想办法获取这个信息,但还没有想到。如果我能做到,我会补充这个答案,但目前这不能帮助你获取非局部变量的值。

更新

不过,直接使用“eval”加上非局部变量的名字是有效的。它必须在嵌套函数中明确声明为非局部,或者以名字硬编码,这样闭包才会真正创建。

def blah():
    b = 1
    def bleh():
        nonlocals = sys._getframe().f_code.co_freevars
        print(f"{eval(nonlocals[0])=}")
        b  # b has to be "touched" so the closure is created.
    return ble

在发现简单的“eval”可以工作之前,我曾这样思考:

看起来,唯一将当前运行的帧与存储非局部变量值的函数对象连接起来的东西是在 Python 解释器的本地部分运行时创建的。我想不出其他方法来获取它,除了使用 ctypes 模块在运行时查看解释器的数据结构,这显然不适合实际的生产代码。

总之:你可以可靠地获取非局部变量的名字。但看起来你无法通过名字字符串获取它们的值(也无法重新绑定它们)。

你可以尝试在 Python 的 bug 跟踪器或 Python-ideas 邮件列表上提出一个关于“非局部变量”的功能请求。

撰写回答