Scope in Python and variable definition

102 views Asked by At

I'm reading a wiki book Python_Programming and I'm a little confused about the piece of code below:

def foo():
    def bar():
        x=5
        return x+y
    y=10
    return bar()
foo()

well, I notice that y is defined outside of bar() and it's used in x+y, which is "before" y is defined. I think similar code will cause a compiling error in C and we have to write something like:

def foo():
    def bar(y):
        x=5
        return x+y
    y=10
    return bar(y)
foo()

Without defining y as a formal parameter in bar(), I don't think the compiler is OK with it. But I guess it's totally fine in Python, which is an interpreted language, right?

Is this something different in interpreted language compared to compiled language? What's the actual process Python uses when interpreting the code at top?


EDIT 1: I think the answer below has made this point very clear, it's about free variable and closure. Here are some links which I think help this question a lot:

SO:python-free-variables-why-does-this-fail
SO:closures-in-python

1

There are 1 answers

3
Martijn Pieters On BEST ANSWER

You are looking at a closure; the Python compiler marks y in bar as a free variable because y is not assigned to.

y is a local in foo and because there is a nested scope that uses that name as a free variable, y is marked as a closure.

When the code runs, Python creates a closure for y immediately when creating the bar() function (it is recreated every time you call foo(); only the bytecode for the function remains unchanged, a constant attached to the foo function code).

Only when bar() is invoked does Python need to look up y, which means dereferencing the closure, and from there dereferencing the current value of y.

You can see all this in action with some introspection and disassembly of the bytecode:

>>> import dis
>>> def foo():
...     def bar(): return y
...     y = 10
...     return bar
... 
>>> foo()
<function foo.<locals>.bar at 0x10d53ce60>
>>> foo()()
10

This builds a function with a closure and returns it without calling. This allows us to introspect the bar() function created by foo().

>>> dis.dis(foo)
  2           0 LOAD_CLOSURE             0 (y) 
              3 BUILD_TUPLE              1 
              6 LOAD_CONST               1 (<code object bar at 0x10d5138a0, file "<stdin>", line 2>) 
              9 LOAD_CONST               2 ('foo.<locals>.bar') 
             12 MAKE_CLOSURE             0 
             15 STORE_FAST               0 (bar) 

  3          18 LOAD_CONST               3 (10) 
             21 STORE_DEREF              0 (y) 

  4          24 LOAD_FAST                0 (bar) 
             27 RETURN_VALUE         

Note how the Python compiler has inserted a LOAD_CLOSURE for y at the top there. MAKE_CLOSURE creates the function object with attached closure, for bar.

>>> dis.dis(foo())
  2           0 LOAD_DEREF               0 (y) 
              3 RETURN_VALUE         

All bar() has to do is dereference the attached closure.

>>> foo.__code__.co_consts
(None, <code object bar at 0x10d5138a0, file "<stdin>", line 2>, 'foo.<locals>.bar', 10)
>>> foo.__code__.co_cellvars
('y',)
>>> foo().__closure__
(<cell at 0x10d5e2c20: int object at 0x10d188940>,)
>>> foo().__closure__[0].cell_contents
10

The closure points to y, and looking up the contents gives you 10, as expected.

Compiled languages could do this as well; someone managed to compile Scheme to C, with preserving the closures: Compiling Scheme to C, for example.