为何Python带参重试装饰器中仅delay_secs需nonlocal声明?
delay_secs触发UnboundLocalError的原因 我实现了一个带参数的Python重试装饰器,代码如下:
import functools import time 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()
当移除nonlocal delay_secs语句时,会触发UnboundLocalError,但max_tries和backoff参数却无此问题。即使调整参数顺序或重命名参数,仍只有delay_secs参数存在作用域问题。使用Python 3.9.2版本、Debian 11系统,恳请解释该现象的原因。
这是由Python的变量作用域解析规则导致的,核心区别在于变量是否在函数内部被赋值:
仅读取变量的情况(
max_tries、backoff)
在wrapper函数中,max_tries只被用来生成循环范围(range(max_tries)),backoff只被用来参与计算(delay_secs *= backoff里的backoff是读取操作)。Python会自动向上查找外层嵌套作用域(这里是retry函数的作用域)中的同名变量,不会将它们标记为wrapper的局部变量,因此不会报错。存在赋值操作的情况(
delay_secs)delay_secs *= backoff本质是delay_secs = delay_secs * backoff,属于赋值操作。Python的规则是:如果函数内部对某个变量进行赋值,就会默认该变量是当前函数的局部变量,不再向上查找外层作用域的同名变量。
但在执行print(f"attempt {attempt} Exception: {e} Sleeping {delay_secs}")时,delay_secs作为局部变量还未被定义(赋值操作在time.sleep(delay_secs)之后),因此触发UnboundLocalError。nonlocal的作用nonlocal delay_secs语句明确告诉Python:这个变量不是wrapper的局部变量,而是来自外层嵌套函数(retry函数的作用域)的变量。这样Python会在外层作用域查找它,同时允许对其进行修改,避免了作用域冲突。
内容的提问来源于stack exchange,提问作者Tyn

