21

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_roundsin the inner function if I use it in a return, but I can't do anything else with it. Why is that?

1
  • This seems possibly hazardous: the first call to the wrapped function will decrement the number of rounds to zero, after which the round count will always be zero. Perhaps you want to initialize a local counter instead? Commented Apr 21, 2015 at 0:25

2 Answers 2

16

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for your answer; it did solve the issue. What is the difference between a global and nonlocal? global does not seem to work in this case.
Nope, a global must live in the global namespace (e.g. at the module level). nonlocal means it could be either global or picked up via a closure.
nonlocal won't pick up globals.
@Veedrac -- You're correct, non-local won't pick up globals. My comment was incorrect.
We can call nr_of_rounds a free variable.
0

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?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.