英文:
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_secs
、max_tries
和backoff
中,只有一个是你在内部函数中写入的。
当尝试检索一个函数内没有赋值的变量的值时,没有歧义:Python会自动在外部范围(内部函数嵌套的函数)、全局范围,然后是内置范围中搜索该变量。(如果在这些范围中找不到它,将引发NameError
错误)。
但是,Python运行时的编译器假定任何在函数中赋值的变量都是局部变量 - 如果想要更改外部或全局范围变量的值,必须显式声明为局部变量(使用nonlocal
或global
关键字)。因此,由于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/
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论