Consider a simple Python decorator with parameters:
def decorator_factory(a=None):
def decorator(func):
def wrapper(*args, **kws):
return func(*args, **kws) + a
return wrapper
return decorator
Sometimes, it is useful to reassign the value of a parameter based on its actual value. This is a common design pattern in Python, especially given the issue with default parameter mutability, but it can be used in other situations, e.g.:
def foo(a, b=None):
if b is None:
b = a
return a + b
However, similar code mimicking an analogous design pattern with decorators, for example the following toy code:
def decorator_factory(a=None):
def decorator(func):
def wrapper(*args, **kws):
y = func(*args, **kws)
if a is None:
a = y
return y + a
return wrapper
return decorator
will raise the following:
UnboundLocalError: local variable 'a' referenced before assignment
How could this be solved?
This is a scoping issue. By reassigning the name, the Python interpreter reserves the reassigned name for local usage, thus shadowing the previous value from the outer scopes, which results in the name being unbound if used before the first assignment.
The simplest solution to this is to never reassign a decorator parameter's name inside the wrapper()
. Just use a different name throughout.
For example:
def decorator_factory(a=None):
def decorator(func):
def wrapper(*args, **kws):
y = func(*args, **kws)
a_ = y if a is None else a
return y + a_
return wrapper
return decorator
@decorator_factory()
def foo(x):
return 2 * x
print(foo(2))
# 8
print(foo(3))
# 12
Note: the nonlocal
statement would avoid raising the UnboundLocalError
, BUT the value of the parameter will persist across multiple function calls, e.g.:
def decorator_factory(a=None):
def decorator(func):
def wrapper(*args, **kws):
nonlocal a
y = func(*args, **kws)
a = y if a is None else a
return y + a
return wrapper
return decorator
@decorator_factory()
def foo(x):
return 2 * x
print(foo(2))
# 8
print(foo(3))
# 10
The last foo()
call gives 10
because the value of a=4
inside the decorated function comes from the previous foo()
call.