I think that I fundamentally don't understand how Python does things like variable scope and name resolution. In particular, the fact that the function broken()
below doesn't work really surprises me. And, though I've fished around the web for awhile looking for a helpful explanation, but I still don't get it. Can anyone explain or link to a good description of how this stuff works in Python, with enough details that it will seem obvious why broken()
doesn't work after reading the relevant materials?
# Why does this code work fine
def okay0():
def foo():
L = []
def bar():
L.append(5)
bar()
return L
foo()
# and so does this
def okay1():
def foo():
def bar():
L.append(5)
L = []
bar()
return L
foo()
# but the following code raises an exception?
def broken():
def foo():
L = []
bar()
return L
def bar():
L.append(5)
foo()
# Example
test_list = [okay0, okay1, broken]
for test_function in test_list:
try:
test_function()
except:
print("broken")
else:
print("okay")
The broken() function throws the following error:
It's because L is defined within foo() and is local to that function. When you try to reference it in some other function such as bar(), it's not going to be defined.
Basically, if you declare a variable within a function, it's going to be local to that function....
The line with
L = ...
infixed
declaresL
infixed
's scope. (Thereturn
before it makes sure the assignment isn't actually executed, just used for scope determination.) The line withnonlocal L
declares thatL
insidefoo
refers to an outer scope'sL
, in this case,fixed
's. Otherwise, since an assignment toL
exists insidefoo
, it would refer to anL
variable insidefoo
.Basically:
nonlocal
orglobal
declaration overrides the scope, instead using the (innermost? outermost?) scope with the variable declared or the global scope, respectively.It is simpler than it looks.
The first case is probably the most obvious:
Here all you have are the regular scope rules.
L
andbar
belong to the same scope, andL
is declared first. Sobar()
can accessL
.The second sample is also similar:
Here both
L
andbar()
belong to the same scope. They are local tofoo()
. It may look different because Python uses dynamic binding. That is, the resolution of the nameL
infoo()
is only resolved when the function is called. By that time, Python already knows thatL
is a local variable to the same function that containsfoo()
, so the acess is valid.However, while Python has dynamic binding, it does not have dynamic scope, so this will fail:
Here, there are two variables named
L
. One is local tofoo()
and another is local tobar()
. Since these functions are not nested and Python does not have dynamic scope, they are two different variables. Becausebar()
does not useL
in an assignment, you get an exception.The most important concept you want to know is
environment evaluation model
, which is simple but powerful.Let me offer you a good material.
If you want to read Python document, you can read 4. Execution model — Python 3.7.4 documentation, it is very terse.
A function defined within another function can access its parent's scope.
In your specific case,
L
is always defined withinfoo()
. On the first two examples,bar()
is defined withinfoo()
as well, so it can accessL
by the rule above (ie,foo()
isbar()
's parent).However, on
broken()
,bar()
andfoo()
are siblings. They know nothing of each others' scopes, sobar()
cannot seeL
.From the documentation:
Now, why does
okay1
work, ifL
is defined textually afterbar()
?Python does not try to resolve the identifiers until it has to actually run the code (dynamic binding, as explained in @Giusti's answer).
When Python gets to execute the function, it sees an identifier
L
and looks for it on the local namespace. On the cpython implementation, it is an actual dictionary, so it looks on a dictionary for a key namedL
.If it does not find it, it checks on the scopes of any enclosing functions, ie the other dictionaries representing the local namespaces of the enclosing functions.
Note that, even if
L
is defined afterbar()
, whenbar()
is called,L
has already been defined. So, whenbar()
is executed,L
already exists on the local namespace offoo()
, which is searched when Python does not seeL
withinbar()
.Supporting piece of the documentation: