Python装饰器中变量的作用域

21

我在Python 3的一个装饰器中遇到了一个非常奇怪的问题。

如果我这样做:

def rounds(nr_of_rounds):
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            return nr_of_rounds
        return inner
    return wrapper

它运行得很好。但是,如果我这样做:

def rounds(nr_of_rounds):
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            lst = []
            while nr_of_rounds > 0:
                lst.append(func(*args, **kwargs))
                nr_of_rounds -= 1
            return max(lst)
        return inner
    return wrapper

我理解为:

while nr_of_rounds > 0:
UnboundLocalError: local variable 'nr_of_rounds' referenced before assignment
换句话说,如果我在返回语句中使用 nr_of_rounds,我可以在内部函数中使用它,但我不能对其进行其他任何操作。为什么会这样?

这似乎可能有危险:包装函数的第一次调用将把回合数减少到零,之后回合计数始终为零。也许你想初始化一个本地计数器? - nneonneo
2个回答

16

由于nr_of_rounds闭包捕获,因此您可以将其视为一个“只读”变量。如果您想要对其进行写入(例如减少它),则需要明确告诉Python - 在这种情况下,Python 3.x中的nonlocal关键字将起作用。

简单地解释一下,当Cpython遇到函数定义时,它会查看代码并判断所有变量是本地还是非本地。本地变量(默认情况下)是任何出现在赋值语句的左侧、循环变量和输入参数中的变量。其他所有名称都是非本地的。这允许一些巧妙的优化1。要像使用本地变量一样使用非本地变量,您需要通过globalnonlocal语句来明确告诉Python。当Python遇到某些它认为应该是本地但实际上不是的东西时,您会得到UnboundLocalError

1Cpython字节码生成器将局部名称转换为数组中的索引,以便局部名称查找(LOAD_FAST字节码指令)与索引数组加上正常字节码开销一样快。


谢谢你的回答,它解决了问题。globalnonlocal 有什么区别?在这种情况下,global 似乎不起作用。 - Ben
1
不行,全局变量 必须 存在于全局命名空间中(例如在模块级别)。nonlocal 意味着它可以是全局的,也可以通过闭包获取。 - mgilson
nonlocal 不会捕获全局变量。 - Veedrac
@Veedrac -- 你是对的,non-local不会捕获全局变量。我的评论是错误的。 - mgilson
2
我们可以称nr_of_rounds为自由变量。 - Ashwini Chaudhary

0

目前,无法对封闭函数作用域中的变量执行相同的操作,但是Python 3引入了一个新关键字"nonlocal",它将以类似全局变量的方式起作用,但是是针对嵌套函数作用域的。
因此,在您的情况下,可以像这样使用:
def inner(*args, **kwargs): nonlocal nr_of_rounds lst = [] while nr_of_rounds > 0: lst.append(func(*args, **kwargs)) nr_of_rounds -= 1 return max(lst) return inner
有关更多信息,请查看 Scoping Rules的简短说明?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接