10

I am learning Python and right now I am on the topic of scopes and nonlocal statement. At some point I thought I figured it all out, but then nonlocal came and broke everything down.

Example number 1:

print( "let's begin" )
def a():
    def b():
        nonlocal x
        x = 20
    b()

a()

Running it naturally fails.
What is more interesting is that print() does not get executed. Why?.

My understanding was that enclosing def a() is not executed until print() is executed, and nested def b() is executed only when a() is called. I am confused...

Ok, let's try example number 2:

print( "let's begin" )
def a():
    if False: x = 10
    def b():
        nonlocal x
        x = 20
    b()

a()

Aaand... it runs fine. Whaaat?! How did THAT fix it? x = 10 in function a is never executed!

My understanding was that nonlocal statement is evaluated and executed at run-time, searching enclosing function's call contexts and binding local name x to some particular "outer" x. And if there is no x in outer functions - raise an exception. Again, at run-time.

But now it looks like this is done at the time of syntax analysis, with pretty dumb check "look in outer functions for x = blah, if there is something like this - we're fine," even if that x = blah is never executed...

Can anybody explain me when and how nonlocal statement is processed?

Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
Vasyl Demianov
  • 305
  • 1
  • 9
  • 1
    [Related question](https://stackoverflow.com/q/46018872/7954504) you may find useful – Brad Solomon Dec 14 '17 at 14:30
  • 2
    putting `nonlocal` requires the variable to actually point to an existing variable, the first one **fails to compile.** – Tadhg McDonald-Jensen Dec 14 '17 at 14:31
  • 1
    If you weren't aware that python was compiled, it may be worth reading [this softwareengineering.SE post](https://softwareengineering.stackexchange.com/questions/136942/why-doesnt-python-need-a-compiler) – Nick is tired Dec 14 '17 at 15:03
  • all languages (except assembly) have to be compiled at some point, you can't give [human text to a CPU](https://stackoverflow.com/questions/12673074/how-should-i-understand-the-output-of-dis-dis) and expect it to do reasonable things! The difference is that `a + b` instead of compiling to "run specific function with `a` and `b`" will compile to something like "look for `+` operation on `a`, if it isn't defined check for reverse `+` on `b`, if that is also undefined raise an error" – Tadhg McDonald-Jensen Dec 14 '17 at 15:25

2 Answers2

4

You can see what the scope of b knows about free variables (available for binding) from the scope of a, like so:

import inspect

print( "let's begin" )

def a():
    if False:
        x = 10

    def b():
        print(inspect.currentframe().f_code.co_freevars)
        nonlocal x
        x = 20

    b()

a()

Which gives:

let's begin
('x',)

If you comment out the nonlocal line, and remove the if statement with x inside, the you'll see the free variables available to b is just ().

So let's look at what bytecode instruction this generates, by putting the definition of a into IPython and then using dis.dis:

In [3]: import dis

In [4]: dis.dis(a)
  5           0 LOAD_CLOSURE             0 (x)
              2 BUILD_TUPLE              1
              4 LOAD_CONST               1 (<code object b at 0x7efceaa256f0, file "<ipython-input-1-20ba94fb8214>", line 5>)
              6 LOAD_CONST               2 ('a.<locals>.b')
              8 MAKE_FUNCTION            8
             10 STORE_FAST               0 (b)

 10          12 LOAD_FAST                0 (b)
             14 CALL_FUNCTION            0
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

So then let's look at how LOAD_CLOSURE is processed in ceval.c.

TARGET(LOAD_CLOSURE) {
    PyObject *cell = freevars[oparg];
    Py_INCREF(cell);
    PUSH(cell);
    DISPATCH();
}

So we see it must look up x from freevars of the enclosing scope(s).

This is mentioned in the Execution Model documentation, where it says:

The nonlocal statement causes corresponding names to refer to previously bound variables in the nearest enclosing function scope. SyntaxError is raised at compile time if the given name does not exist in any enclosing function scope.

ely
  • 74,674
  • 34
  • 147
  • 228
  • "This is mentioned in the Execution Model documentation..." Is as far as I can tell the answer to the question in itself, you may want to put it at the top of your answer to have the form "the documentation says this isn't allowed, let me explain why" – Tadhg McDonald-Jensen Dec 14 '17 at 15:50
  • Thanks for the link to the Execution Model. I guess Python's execution process is far more complex than "read a line, execute statement in it, read next line..." Yet I don't quite understand necessity for such complexities... – Vasyl Demianov Dec 15 '17 at 14:10
  • What is the purpose of the if statement? I don't notice any difference when I remove it in terms of output. – Yu Chen Jun 11 '20 at 16:54
  • @YuChen the `if False` statement is there to ensure `x` doesn't actually get assigned in code execution. The idea of the original question is to figure out why can the scope of `b()` "see" the variable `x` when it is never assigned in the parent scope. When you include the `if False` bit, you can see from `co_freevars` that the nonlocal `x` is recognized as a freevariable in the enclosing scope, because this is resolved *at compile time* regardless of whether the branch of code at execution time actually encounters and assigns `x` or not. – ely Jun 12 '20 at 00:56
  • Got it, now I understand, appreciate that explanation about runtime versus compile time – Yu Chen Jun 12 '20 at 00:57
3

First, understand that python will check your module's syntax and if it detects something invalid it raises a SyntaxError which stops it from running at all. Your first example raises a SyntaxError but to understand exactly why is pretty complicated although it is easier to understand if you know how __slots__ works so I will quickly introduce that first.


When a class defines __slots__ it is basically saying that the instances should only have those attributes so each object is allocated memory with space for only those, trying to assign other attributes raises an error

class SlotsTest:
    __slots__ = ["a", "b"]

x = SlotsTest()

x.a = 1 ; x.b = 2
x.c = 3 #AttributeError: 'SlotsTest' object has no attribute 'c'

The reason x.c = 3 can't work is that there is no memory space to put a .c attribute in.

If you do not specify __slots__ then all instances are created with a dictionary to store the instance variables, dictionaries do not have any limitations on how many values they contain

class DictTest:
    pass

y = DictTest()
y.a = 1 ; y.b = 2 ; y.c = 3
print(y.__dict__) #prints {'a': 1, 'b': 2, 'c': 3}

Python functions work similar to slots. When python checks the syntax of your module it finds all variables assigned (or attempted to be assigned) in each function definition and uses that when constructing frames during execution.

When you use nonlocal x it gives an inner function access to a specific variable in the outer function scope but if there is no variable defined in the outer function then nonlocal x has no space to point to.

Global access doesn't run into the same issue since python modules are created with a dictionary to store its attributes. So global x is allowed even if there is no global reference to x

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
  • I recognize this is a badly formed answer, I'm not sure how else to explain this without either talking about `slots` or how python handles stack memory which would be far worse. – Tadhg McDonald-Jensen Dec 14 '17 at 14:48
  • It's not that badly formed as you think it is - it gave me some understanding of Python. So, basically, the first phase of execution of a Python script - is syntax analysis, when Python machine determines what can actually later exist? Isn't this contrary to the whole "dynamic" paradigm?.. What about variables created via exec()? – Vasyl Demianov Dec 15 '17 at 11:35
  • 1
    Regardless of how it is interpreted, it will need to convert the code to something it can execute at some point, doing it per-file instead of per-line makes sense because if you have a long script it's nice to know you forgot a bracket on the last line *before* it tries to run it. As well python makes clear guarantee that globals will be dynamic like dictionaries (see `help(globals)` note) and does not guarantee this for locals or closures (see `help(locals)` note) – Tadhg McDonald-Jensen Dec 15 '17 at 20:09
  • 2
    as for `exec`, it must use a **dictionary** for its local and global namespace, if none are provided it uses the current global one and a new dictionary for local variables or if one is given it uses it for globals and locals. In any case, it can't directly interact with the namespace of a function because it doesn't store local variables in a dictionary in the default C implementation. – Tadhg McDonald-Jensen Dec 15 '17 at 20:12