79

Suppose I have the following python code:

def outer():
    string = ""
    def inner():
        string = "String was changed by a nested function!"
    inner()
    return string

I want a call to outer() to return "String was changed by a nested function!", but I get "". I conclude that Python thinks that the line string = "string was changed by a nested function!" is a declaration of a new variable local to inner(). My question is: how do I tell Python that it should use the outer() string? I can't use the global keyword, because the string isn't global, it just lives in an outer scope. Ideas?

Ord
  • 5,693
  • 5
  • 28
  • 42

4 Answers4

97

In Python 3.x, you can use the nonlocal keyword:

def outer():
    string = ""
    def inner():
        nonlocal string
        string = "String was changed by a nested function!"
    inner()
    return string

In Python 2.x, you could use a list with a single element and overwrite that single element:

def outer():
    string = [""]
    def inner():
        string[0] = "String was changed by a nested function!"
    inner()
    return string[0]
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • +1 As for why the latter works: Item assignment (along with attribute assignment) is not variable assignment - `x[...] = ...` just gets `x` from whatever scope it would be in anyway, and call's its `__setitem__` method. An equally valid, but initially more verbose option is creating an object (of a user-defined class, as `object()` won't allow adding attributes) whose sole purpose is holding an attribute. –  Oct 28 '11 at 23:51
  • 2
    You can create such an "namespace object" using `x = type("", (), {})()`. :) – Sven Marnach Oct 28 '11 at 23:55
  • So, `nonlocal` means: "look for this variable in the outer scope" - if the variable was not found in the outer() scope, would Python keep looking in enclosing scopes? Would Python eventually try to find the variable in the global scope? – Ord Oct 29 '11 at 00:05
  • Also, is there really NO way to rebind the outer() string in Python 2.x? It seems silly that I would have access a variable defined in an outer scope, and be able to call its methods, but not to reassign the variable to point to something else... – Ord Oct 29 '11 at 00:07
  • 2
    @Ord: `nonlocal` will look in all enclosing scopes except for the global scope -- that's what `global` is for after all. There is no other way than the above solutions in Python 2.x -- that's why `nonlocal` was introduced. And Python semantics say that assignment inside a function automatically makes a variable local to that function -- that's a sensible choice given that you don't declare variables in Python and the compiler somehow has to figure out the scope of a variable. – Sven Marnach Oct 29 '11 at 00:13
  • In 2.x, use a `dict` instead of a `list` if you want to access+modify multiple nonlocal variables *and* keep your code fairly clean. – nitsas Nov 18 '14 at 21:13
28

You can also get around this by using function attributes:

def outer():
    def inner():
        inner.string = "String was changed by a nested function!"
    inner.string = ""
    inner()
    return inner.string

Clarification: this works in both python 2.x and 3.x.

crunk1
  • 2,560
  • 1
  • 26
  • 32
5

This happens to me way too often, when I was writing a function and I suddenly realize that it could be a good idea to have a smaller helper function, but not really useful anywhere else. which naturally makes me want to define it inside as a nested function.

but I had experience with JAVA anonymous object(ie: define a runnable), and the rule was that the anonymous object makes a hard copy of its outer environment, in this case variables of the outer scope. Thus if the outer variable is a immutable (int,char), they can not be modified by anonymous object as they are copied by value whereas if its a mutable (collection, objects), they can be changed...since they are copied by "pointer" (their address in memory)

if you know about programming, think of it as pass by value and pass by reference.

in python, it's very much the same. x=123 is an assignment, they give the variable x a new meaning (not modify the old x), list[i]/dict[key] are object access operations, they really modify things

to conclude, you need a mutable object...in order to modify (even though you can access a tuple using [], you can not use it here since its not mutable)

watashiSHUN
  • 9,684
  • 4
  • 36
  • 44
2

To add to Sven's answer:

In Python 2.x, you can only read outer scope variables from the inner scope. Assigning will just create a new local (i.e. inner scope) variable that hides the outer scope one.

If you want to read and modify, you can use a dict to hold your variables in the outer scope, and then access them via the dict in the inner scope, also keeping your code fairly clean and readable in the presence of multiple outer scope vars:

def outer():
    # hold some text, plus the number of spaces in the text
    vars = {'text': 'Some text.', 'num_spaces': 1}
    def inner():
        # add some more text
        more_text = ' Then some more text.'
        vars['text'] += more_text
        # keep track of the number of spaces
        vars['num_spaces'] += more_text.count(' ')
    inner()
    return vars['text'], vars['num_spaces']

output:

>>> outer()
('Some text. Then some more text.', 5)
Community
  • 1
  • 1
nitsas
  • 972
  • 7
  • 17