21

The following code raises an UnboundLocalError:

def foo():
    i = 0
    def incr():
        i += 1
    incr()
    print(i)

foo()

Is there a way to accomplish this?

Maroun
  • 94,125
  • 30
  • 188
  • 241
shooqie
  • 950
  • 7
  • 17
  • 4
    You can pass `i` as an argument. – Maroun Dec 23 '15 at 08:12
  • 1
    You can update value of `i` as `i = incr()` – Tanveer Alam Dec 23 '15 at 08:14
  • 2
    @MarounMaroun Judgind from this question, Python what, doesn't have closures? 0_o' I'm not familiar with it, though. Or is it because `def` defines a function in the global namespace? I'm just curious, and don't want to learn Python just for this. :) – hijarian Dec 23 '15 at 15:44

8 Answers8

30

Use nonlocal statement

def foo():
    i = 0
    def incr():
        nonlocal i
        i += 1
    incr()
    print(i)

foo()

For more information on this new statement added in python 3.x, go to https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement

piglei
  • 1,188
  • 10
  • 13
20

You can use i as an argument like this:

def foo():
    i = 0
    def incr(i):
        return i + 1
    i = incr(i)
    print(i)

foo()
Remi Guan
  • 21,506
  • 17
  • 64
  • 87
  • 6
    Great answer, as this does not depend on Python 3, like the other `nonlocal` solutions. – Burhan Khalid Dec 23 '15 at 08:17
  • 2
    The name `incr` is not well chosen for a function that returns one more than its argument without any side effects. – Marc van Leeuwen Dec 23 '15 at 15:40
  • The name of this functional version of `incr` should probably be `succ` :) – Jacob Krall Dec 23 '15 at 20:16
  • @MarcvanLeeuwen: Huh, seems so. Any good ideas or use `succ` as Jacob suggested? – Remi Guan Dec 23 '15 at 23:04
  • @MarcvanLeeuwen, could you elaborate on this? Why is it not a good name? – shooqie Dec 24 '15 at 11:09
  • 2
    @shooqie: Because by convention names that are (abbreviation for) verbs suggest a procedure called for its side effect, not a pure function called for its return value only, as is the case here. A noun like `succ`(essor), or even a description `one_plus`, would be more appropriate here. – Marc van Leeuwen Dec 24 '15 at 14:21
  • `incr` is short for `increment`, which is exactly what the function does. – shooqie Dec 24 '15 at 17:15
  • No, it doesn't. To increment something is to *change it* by increasing its value by one. This function *determines the result* of increasing something's value by one, creating a *new thing*. – Karl Knechtel Jul 27 '22 at 19:56
9

See 9.2. Python Scopes and Namespaces:

if no global statement is in effect – assignments to names always go into the innermost scope.

Also:

The global statement can be used to indicate that particular variables live in the global scope and should be rebound there; the nonlocalstatement indicates that particular variables live in an enclosing scope and should be rebound there.

You have many solutions:

  • Pass i as an argument ✓ (I would go with this one)
  • Use nonlocal keyword

Note that in Python2.x you can access non-local variables but you can't change them.

Maroun
  • 94,125
  • 30
  • 188
  • 241
6

People have answered your question but no one seems to address why exactly this is happening.

The following code raises an UnboundLocalError

So, why? Let's take a quote from the FAQ:

When you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope.

Inside your nested function you are performing an assignment with the += operator. What this means is that i += 1 will approximately perform i = i + 1 (from a binding perspective). As a result i in the expression i + 1 will be searched for in the local scope (because it is used in the assignment statement) for function incr where it will not be found, resulting in an UnboundLocalError: reference before assignment .

There's many ways you can tackle this and python 3 is more elegant in the approach you can take than python 2.

Python 3 nonlocal:

The nonlocal statements tells Python to look for a name in the enclosing scope (so in this case, in the scope of function foo()) for name references:

def foo():
    i = 0
    def incr():
        nonlocal i
        i +=1
    incr()
    print(i)

Function attributes Python 2.x and 3.x:

Remember that functions are first class objects, as such they can store state. Use function attributes to access and mutate function state, the good thing with this approach is it works on all pythons and doesn't require global, nonlocal statements.

def foo():
    foo.i = 0
    def incr():
        foo.i +=1
    incr()
    print(foo.i)

The global Statement (Python 2.x, 3.x):

Really the ugliest of the bunch, but gets the job done:

i = 0
def foo():
    def incr():
        global i
        i += 1
    incr()
    print(i)

foo()

The option of passing an argument to the function gets the same result but it doesn't relate to mutating the enclosing scope in the sense nonlocal and global are and is more like the function attributes presented. It is creating a new local variable in function inc and then re-binding the name to i with the i = incr(i) but, it does indeed get the job done.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
4

In Python, int are immutable. So you could put your int in a mutable object.

def foo():
    i = 0
    obj = [i]
    def incr(obj):
        obj[0]+=1
    incr(obj)
    print(obj[0])

foo()
Luis González
  • 3,199
  • 26
  • 43
  • 1
    With this approach, it's not necessary to pass `obj` as an argument to `incr`; it works fine as a closure. – tsbertalan Dec 23 '15 at 23:22
2

You can make i global and use it.

i = 0
def foo():
    def incr():
        global i
        i += 1
    incr()
    print(i)

foo()

but the most preferred way is to pass i as a param to incr

def foo():
    i = 0
    def incr(arg):
        arg += 1
        return arg
    i = incr(i)
    print(i)

foo()
anand
  • 1,506
  • 14
  • 28
0

you could also use a lambda function:

def foo():
  i=0
  incr = lambda x: x+1
  print incr(i)

foo()

I think the code is cleaner this way

Pandrei
  • 4,843
  • 3
  • 27
  • 44
0

Simple function attributes will not work in this case.

>>> def foo():
...     foo.i = 0
...     def incr():
...         foo.i +=1
...     incr()
...     print(foo.i)
... 
>>> 
>>> 
>>> foo()
1
>>> foo()
1
>>> foo()
1

You re assign your foo.i to 0 for every call of foo. It better to use hassattr. But code become more complicated.

>>> def foo():
...     if not hasattr(foo, 'i'):
...         foo.i = 0
...     def incr():
...         foo.i += 1
...         return foo.i
...     i = incr()
...     print(i)
... 
>>> 
>>> 
>>> foo()
1
>>> foo()
2
>>> foo()
3

Also you can try this idea:

>>> def foo():
...     def incr():
...         incr.i += 1
...         return incr.i
...     incr.i = 0
...     return incr
... 
>>> a = foo()
>>> a()
1
>>> a()
2
>>> a()
3
>>> a()
4

May be it is more handy to wrap you incr into decorator.