Do you remember when we talked about scopes and namespaces in the first chapter? We're going to expand on that concept now. Finally, we can talk about functions and this will make everything easier to understand. Let's start with a very simple example.
scoping.level.1.py
def my_function(): test = 1 # this is defined in the local scope of the function print('my_function:', test) test = 0 # this is defined in the global scope my_function() print('global:', test)
I have defined the name test
in two different places in the previous example. It is actually in two different scopes. One is the global scope (test = 0
), and the other is the local scope of the function my_function
(test = 1
). If you execute the code, you'll see this:
$ python scoping.level.1.py my_function: 1 global: 0
It's clear that test = 1
shadows the assignment test = 0
in my_function
. In the global context, test
is still 0
, as you can see from the output of the program but we define the name test
again in the function body, and we set it to point to an integer of value 1
. Both the two test
names therefore exist, one in the global scope, pointing to an int
object with value 0, the other in the my_function
scope, pointing to an int
object with value 1. Let's comment out the line with test = 1
. Python goes and searches for the name test
in the next enclosing namespace (recall the LEGB rule: Local, Enclosing, Global, Built-in described in Chapter 1, Introduction and First Steps – Take a Deep Breath) and, in this case, we will see the value 0
printed twice. Try it in your code.
Now, let's raise the stakes here and level up:
scoping.level.2.py
def outer(): test = 1 # outer scope def inner(): test = 2 # inner scope print('inner:', test) inner() print('outer:', test) test = 0 # global scope outer() print('global:', test)
In the preceding code, we have two levels of shadowing. One level is in the function outer
, and the other one is in the function inner
. It is far from rocket science, but it can be tricky. If we run the code, we get:
$ python scoping.level.2.py inner: 2 outer: 1 global: 0
Try commenting out the line test = 1
. What do you think the result will be? Well, when reaching the line print('outer:', test)
, Python will have to look for test
in the next enclosing scope, therefore it will find and print 0
, instead of 1
. Make sure you comment out test = 2
as well, to see if you understand what happens, and if the LEGB rule is clear, before proceeding.
Another thing to note is that Python gives you the ability to define a function in another function. The inner function's name is defined within the namespace of the outer function, exactly as would happen with any other name.
Going back to the preceding example, we can alter what happens to the shadowing of the test name by using one of these two special statements: global
and nonlocal
. As you can see from the previous example, when we define test = 2
in the function inner
, we overwrite test
neither in the function outer
, nor in the global scope. We can get read access to those names if we use them in a nested scope that doesn't define them, but we cannot modify them because, when we write an assignment instruction, we're actually defining a new name in the current scope.
How do we change this behavior? Well, we can use the nonlocal
statement. According to the official documentation:
"The
nonlocal
statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals."
Let's introduce it in the function inner
, and see what happens:
scoping.level.2.nonlocal.py
def outer(): test = 1 # outer scope def inner(): nonlocal test test = 2 # nearest enclosing scope print('inner:', test) inner() print('outer:', test) test = 0 # global scope outer() print('global:', test)
Notice how in the body of the function inner
I have declared the test
name to be nonlocal
. Running this code produces the following result:
$ python scoping.level.2.nonlocal.py inner: 2 outer: 2 global: 0
Wow, look at that result! It means that, by declaring test
to be nonlocal
in the function inner
, we actually get to bind the name test
to that declared in the function outer
. If we removed the nonlocal
test
line from the function inner
and tried the same trick in the function outer
, we would get a SyntaxError
, because the nonlocal
statement works on enclosing scopes excluding the global one.
Is there a way to get to that test = 0
in the global namespace then? Of course, we just need to use the global
statement. Let's try it.
scoping.level.2.global.py
def outer(): test = 1 # outer scope def inner(): global test test = 2 # global scope print('inner:', test) inner() print('outer:', test) test = 0 # global scope outer() print('global:', test)
Note that we have now declared the name test
to be global
, which will basically bind it to the one we defined in the global namespace (test = 0
). Run the code and you should get the following:
$ python scoping.level.2.global.py inner: 2 outer: 1 global: 2
This shows that the name affected by the assignment test = 2
is now the global
one. This trick would also work in the outer
function because, in this case, we're referring to the global scope. Try it for yourself and see what changes, get comfortable with scopes and name resolution, it's very important.