I'm having a very weird problem in a Python 3 decorator.
If I do this:
def rounds(nr_of_rounds):
def wrapper(func):
@wraps(func)
def inner(*args, **kwargs):
return nr_of_rounds
return inner
return wrapper
it works just fine. However, if I do this:
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
I get:
while nr_of_rounds > 0:
UnboundLocalError: local variable 'nr_of_rounds' referenced before assignment
In other words, I can use nr_of_rounds
in the inner function if I use it in a return, but I can't do anything else with it. Why is that?
Since nr_of_rounds
is picked up by the closure, you can think of it as a "read-only" variable. If you want to write to it (e.g. to decrement it), you need to tell python explicitly -- In this case, the python3.x nonlocal
keyword would work.
As a brief explanation, what Cpython does when it encounters a function definition is it looks at the code and decides if all the variables are local or non-local. Local variables (by default) are anything that appear on the left-hand side of an assignment statement, loop variables and the input arguments. Every other name is non-local. This allows some neat optimizations1. To use a non-local variable the same way you would a local, you need to tell python explicitly either via a global
or nonlocal
statement. When python encounters something that it thinks should be a local, but really isn't, you get an UnboundLocalError
.
1The Cpython bytecode generator turns the local names into indices in an array so that local name lookup (the LOAD_FAST bytecode instruction) is as fast as indexing an array plus the normal bytecode overhead.
Currently there is no way to do the same for variables in enclosing function scopes, but Python 3 introduces a new keyword, "nonlocal" which will act in a similar way to global, but for nested function scopes.
so in your case just use like:
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
For more info Short Description of the Scoping Rules?