1

This a follow up question to this.

I have two code snippets, one which works and one which does not. The following works:

def decorator(f):
    calls_left = [2]
    def inner(*args,**kwargs):
        if calls_left[0] == 0:
            print "limit reached, reseting"
            calls_left[0] -= 1
        return f(*args,**kwargs)
    return inner

@decorator
def foo():
    print "Function called"

foo()
foo()
foo()

Produces:

Function called 
Function called
limit reached, reseting
Function called

However, if I change calls_left to a simpe integer, I get an error:

def decorator(f):
    calls_left = 2
    def inner(*args,**kwargs):
        if calls_left == 0:
            print "limit reached, reseting"
            calls_left -= 1
        return f(*args,**kwargs)
    return inner

@decorator
def foo():
    print "Function called"

foo()



File "decorator_test.py", line 40, in <module>
    foo()
File "decorator_test.py", line 25, in inner
    if calls_left == 0:
UnboundLocalError: local variable 'calls_left' referenced before assignment

I understand that the list is mutable and the integer is not, but it seems like neither snippet should work. In the first case, my understanding is that calls_left is redefined when foo is called, so progressive incrementing shouldn't work. In the second case, I would expect foo to have access to calls_left.

Can someone explain this behavior, and the scoping issues surrounding my two calls_left objects?

Community
  • 1
  • 1
wils484
  • 275
  • 1
  • 3
  • 14

1 Answers1

0

calls_left doesn't get redefined in the first example ... It only has a single element mutated.

In the second case, when python first looks at the functions code, it sees calls_left has an assignment to it, so it (wrongly) assumes that calls_left is in the current scope. When you do if calls_left == 0, it tries to assign a new value to calls_left in the current scope (looking for calls_left in the current scope to calculate the new value). When it doesn't find calls_left in the current scope, it raises the exception.

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • While your statement is obviously true, it doesn't explain why an error is raised on the second case. Care to explain ? – Nir Alfasi Jun 16 '14 at 17:07
  • @alfasin -- updated. Does that explain it better? – mgilson Jun 16 '14 at 17:36
  • Thanks now it's clearer, thank you! the only thing that still bothers me is that the OP says he got the error on line `if calls_left == 0:` not on `calls_left -= 1`. Any ideas ? – Nir Alfasi Jun 16 '14 at 17:39
  • @alfasin -- Good point. For some reason, I always get the way this works wrong. I've updated. Actually, python thinks that `calls_left` is _local_ because it sees the assignment when it parses the function. But then at the line `calls_left -= 1` (which is roughly `calls_left = calls_left - 1`), tries to find `calls_left` on the RHS of the equation in the local scope since it has an assignment to it in the function. – mgilson Jun 16 '14 at 17:48
  • `calls_left == 0` does not assign a new value to calls_left. The only thing that makes sense to me is (and please correct me if I misunderstood): on the first scan of the function, interpreter detects that there is an assignment to the variable `calls_left` (`calls_left -= 1`) and thus assumes it's a local variable and marks it as such. On the second pass (execution) it fails (on `calls_left == 0:`) cause there is no such local variable. While, when we declare `calls_left` as a list, we don't try to assign anything to it (only changing its items) - so the interpreter doesn't assume as local. – Nir Alfasi Jun 16 '14 at 18:58
  • And the reason that the interpreter assumes its local is: according to the [spec](https://docs.python.org/2/tutorial/classes.html#python-scopes-and-namespaces): "...all variables found outside of the innermost scope are read-only..." – Nir Alfasi Jun 16 '14 at 19:01