18

I've got this piece of code:

#!/usr/bin/env python

def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache=[v]
    return cache
  return match
m = get_match()
m(1)

if I run it, it says:

UnboundLocalError: local variable 'cache' referenced before assignment

but if I do this:

#!/usr/bin/env python

def get():
  y = 1
  def m(v):
    return y + v
  return m

a=get()
a(1)

it runs.

Is there something with list? or my code organizing is wrong?

nemo
  • 12,241
  • 3
  • 21
  • 26
  • 1
    Your second example has no line analogous to `cache=[v]`. This makes Python treat the variable as a local variable, making earlier references within the function illegal. – Steven Rumbalski Aug 23 '12 at 12:59
  • Shouldn't your condition read `if v in cache:`? Otherwise this is a "cache" that will only contain the parameter from the very first call – Tobias Kienzler Aug 23 '12 at 13:15
  • I can't help but associate your code with memoization, is that what you intend to do? In that case have a look at http://stackoverflow.com/a/3377272/321973 – Tobias Kienzler Aug 23 '12 at 15:41
  • Does this answer your question? [UnboundLocalError with nested function scopes](https://stackoverflow.com/questions/2609518/unboundlocalerror-with-nested-function-scopes) – user202729 Feb 01 '21 at 01:19

4 Answers4

32

The problem is that the variable cache is not in the scope of the function match. This is not a problem if you only want to read it as in your second example, but if you're assigning to it, python interprets it as a local variable. If you're using python 3 you can use the nonlocal keyword to solve this problem - for python 2 there's no simple workaround, unfortunately.

def f():
    v = 0

    def x():
        return v    #works because v is read from the outer scope

    def y():
        if v == 0:  #fails because the variable v is assigned to below
            v = 1

    #for python3:
    def z():
        nonlocal v  #tell python to search for v in the surrounding scope(s)
        if v == 0:
            v = 1   #works because you declared the variable as nonlocal

The problem is somewhat the same with global variables - you need to use global every time you assign to a global variable, but not for reading it.

A short explanation of the reasons behind that: The python interpreter compiles all functions into a special object of type function. During this compilation, it checks for all local variables the function creates (for garbage collection etc). These variable names are saved within the function object. As it is perfectly legal to "shadow" an outer scopes variable (create a variable with the same name), any variable that is assigned to and that is not explicitly declared as global (or nonlocal in python3) is assumed to be a local variable.

When the function is executed, the interpreter has to look up every variable reference it encounters. If the variable was found to be local during compilation, it is searched in the functions f_locals dictionary. If it has not been assigned to yet, this raises the exception you encountered. If the variable is not assigned to in the functions scope and thus is not part of its locals, it is looked up in the surrounding scopes - if it is not found there, this raises a similar exception.

l4mpi
  • 5,103
  • 3
  • 34
  • 54
  • No problem :) I've expanded it a bit to describe what happens in the interpreter. – l4mpi Aug 23 '12 at 13:31
  • 2
    @Brandon it's a simple but effective performance optimization - local variables will be stored as a pointer to a PyObject in an array of the PyFrameObject; getting or setting a local variable is as easy as indexing the array. Variables of outer scopes have to be looked up through a way more complicated process; see [the source](http://hg.python.org/cpython/file/3fe2fd4ffa32/Python/ceval.c#l1353) for details (LOAD_FAST for locals, LOAD_NAME/GLOBAL/DEREF otherwise). And I think it's good that you have to declare when you want to overwrite a global: like the Zen states, `explicit > implicit`. – l4mpi Dec 07 '13 at 14:31
  • it seems python could automatically look up the non-local scope when the variable is not defined. – Strin Jan 08 '16 at 06:54
7

Accessing a variable is different from assigning it.

You have a similar situation with global variables. You can access them in any function, but if you try to assign to it without the global statement, it will redeclare it in the local context.

Unfortunately for local functions there is no equivalent of the global statement, but you can bypass the redeclaration by replacing

cache=[v]

with:

cache[:] = [v]
Mihai Stan
  • 1,052
  • 6
  • 7
  • In Python 3 there is the `nonlocal` keyword. – Steven Rumbalski Aug 23 '12 at 13:03
  • I don't get your replacement - cache[:] creates a copy of the list (which obviously works because you now don't assign to cache anymore) and assigns to this copy - but the copy is immediately discarded, so you're not actually assigning to the variable. So while your replacement fixes the error message, it doesn't actually do anything useful... – l4mpi Aug 23 '12 at 13:09
  • 1
    @l4mpi: Incorrect. `cache[:]` creates a copy when it's on the the *right side* of the assignment, but when it's on the *left side*, it indicates assignment to a slice, which is a mutating operation. To see this in action, paste this into the interpreter: `a=[]; a_id = id(a); a[:] = [1,2,3]; id(a) == a_id`. It will return `True`. – Steven Rumbalski Aug 23 '12 at 14:37
  • @StevenRumbalski, thanks for the info, I wasn't aware of this usage of the slice operation - upvoted the answer. Ok, so this works because it calls `__setslice__` on the list, but still has some limitations: Obviously it only works for lists and not arbitrary variable types; it would fail if the variable had been initialized with `None` instead of `[]`; the right side can only contain iterables meaning you can't set the variable to None or change its reference to another type or even another list (as `a[:]=b` where b is an iterable only copies b, so modifying a won't effect b). – l4mpi Aug 23 '12 at 16:06
3

Since Python sees cache=[v] - assignment to cache, it treats it as local variable. So the error is pretty reasonable - no local variable cache was defined prior to its usage in if statement.

You probably want to write it as:

def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache.append(v)
    return cache
  return match
m = get_match()
m(1)

Highly recommended readings: Execution Model - Naming and binding and PEP 227 - Statically Nested Scopes

Roman Bodnarchuk
  • 29,461
  • 12
  • 59
  • 75
1

Replace

cache=[]
def match(v):

with

def match(v,cache=[])

Explanation: Your code declares cache as a variable of get_match, which the returned match(v) knows nothing about (due to the following assignment). You do however want cache to be part of match's namespace.

I know this way a "malevolent" user could redefine cache, but that's their own mistake. Should this be an issue though, the alternative is:

def match(v):
     try:
         if cache:
             return cache
     except NameError:
         cache = []
     ...

(See here)

Community
  • 1
  • 1
Tobias Kienzler
  • 25,759
  • 22
  • 127
  • 221
  • Catching `NameError` will not work for the OP, because it will always be raised. See [this contrived example](http://codepad.org/ElMP6k97). – Steven Rumbalski Aug 23 '12 at 14:51
  • @StevenRumbalski Thanks, I don't yet understand why but I'll try to... Anyway, [the other way](http://codepad.org/xKQDux9c) works – Tobias Kienzler Aug 23 '12 at 15:12
  • [here](http://codepad.org/tMf79I7C) is the code including the cache the way I understand a cache should work... (`if v in cache` and `cache.append(v)` instead of `if cache` and `cache = [v]`) – Tobias Kienzler Aug 23 '12 at 15:33