Python装饰器参数作用域

huangapple go评论63阅读模式
英文:

Python decorator parameter scope

问题

I've implemented a retry decorator with some parameters:

def retry(max_tries: int = 3, delay_secs: float = 1, backoff: float = 1.5):
    print("level 1:", max_tries, delay_secs, backoff)
    def decorator(func):
        print("level 2:", max_tries, delay_secs, backoff)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal delay_secs  ## UnboundLocalError if remove this line
            print("level 3:", max_tries, delay_secs, backoff)
   
            for attempt in range(max_tries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"attempt {attempt} Exception: {e}  Sleeping {delay_secs}")
                    time.sleep(delay_secs)
                    delay_secs *= backoff
            print("exceeded maximun tries")
            raise e
    
        return wrapper
    
    return decorator

@retry(max_tries=4, delay_secs=1, backoff=1.25)
def something():
    raise Exception("foo")

something()
英文:

I've implemented a retry decorator with some parameters:

def retry(max_tries: int = 3, delay_secs: float = 1, backoff: float = 1.5):
    print("level 1:", max_tries, delay_secs, backoff)
    def decorator(func):
        print("level 2:", max_tries, delay_secs, backoff)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal delay_secs  ## UnboundLocalError if remove this line
            print("level 3:", max_tries, delay_secs, backoff)

            for attempt in range(max_tries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"attempt {attempt} Exception: {e}  Sleeping {delay_secs}")
                    time.sleep(delay_secs)
                    delay_secs *= backoff
            print("exceeded maximun tries")
            raise e

        return wrapper

    return decorator

@retry(max_tries=4, delay_secs=1, backoff=1.25)
def something():
    raise Exception("foo")

something()

If I remove this line, I got UnboundLocalError

nonlocal delay_secs

But that only happens for delay_secs, NOT for max_tries or backoff!
I tried reordering/renaming the params and still that delay param is problematic.

Can you help me understand why that parameter out of scope within the wrapper function but the other 2 parameters are just fine?

Python: 3.9.2

OS: Debian 11 Linux

答案1

得分: 1

答案几乎可以说是微不足道的:在这3个变量中,delay_secsmax_triesbackoff中,只有一个是你在内部函数中写入的。

当尝试检索一个函数内没有赋值的变量的值时,没有歧义:Python会自动在外部范围(内部函数嵌套的函数)、全局范围,然后是内置范围中搜索该变量。(如果在这些范围中找不到它,将引发NameError错误)。

但是,Python运行时的编译器假定任何在函数中赋值的变量都是局部变量 - 如果想要更改外部或全局范围变量的值,必须显式声明为局部变量(使用nonlocalglobal关键字)。因此,由于delay_secs的值已更改,没有使用nonlocal声明,Python假定它是一个局部变量。当它尝试检索其值时,无论是在您的print调用中,还是在您使用*=运算符时的隐式读取(它必须检索原始值以进行乘法运算),都还没有分配给它本地值,因此会出现错误。Python "知道",因为在编译时标记它,该函数应该在某个时刻有一个名为delay_secs的局部变量 - 因此您不会得到NameError,但由于没有值可读取,它会引发UnboundLocalError错误。

请注意,一些其他语言会在内部函数中使用外部范围的变量进行读取,直到在内部函数中赋值后,才会创建一个局部变量 - 这不是Python的工作方式,关于这一点的原理在创建nonlocal关键字的PEP中有写明:https://peps.python.org/pep-3104/

英文:

The answer is almost trivial:
out of the 3 variables, delay_secs, max_tries and backoff, there is only one you write to in your inner function.

When one tries to retrieve the value of a variable for which there is no assignment inside a function, there is no ambiguity: Python automatically searches for that variable in outer scopes (functions where the inner function is nested in), then in the global, and finally the built-in scope. (If it is not found in any of those, a NameError is raised).

But the compiler of the Python runtime assumes that any variable that is assigned to in a function is a local variable - if one wants to change the values of variables in outer or global scopes, they have to be explicitly declared as so (with the nonlocal or global keywords, respectivelly).

So, since the value of delay_secs is changed, without the nonlocal declaration, Python assumes that is a local variable. When it tries to retrieve its value, both in your print call, and then in the implicit read when you use the *= operator (it has to retrieve the original value in order to multiply it), there is no local value assigned to it yet, so the error. Python "knows", because it marks it at compile time, that at some point that function should have a local variable named delay_secs - so you don't get a NameError - but since there is no value to be read, it raises UnboundLocalError instead.

Note that some other languages would use the outer scope variable for reading, until it was assigned in the inner function, at which point a local variable would be created - this is not how Python works, and the rationale for that is written in the PEP which created the nonlocal keyword itself: https://peps.python.org/pep-3104/

huangapple
  • 本文由 发表于 2023年5月18日 06:28:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76276576.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定