Scope¶

Dynamic vs Lexical Scope¶

An identifier such as variable name is "in scope" if it is "bound" (points) to a value.

Identifiers exist in "namespaces". Python namespaces are like dictionaries in that identifiers can be added or removed at execution time. This is called "dynamic scope".

This differs from languages such as C where the scope of an identifier is defined at compile time and is determined by the location of the identifier's declaration in the program. This is called "lexical scope".

In [1]:
a=1
try:
    print(a)
except Exception as e:
    print(e)
    
# locals() and globals() functions return dictionaries of variables.
# we can manipulate these dictiionaries to add/access/remove variables:

locals()['b'+str(3)]=4
print(b3)
globals()['c'+'d']=5
print(cd)
del(locals()['b3'])
print(b3)
1
4
5
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [1], in <cell line: 15>()
     13 print(cd)
     14 del(locals()['b3'])
---> 15 print(b3)

NameError: name 'b3' is not defined

Local Variables¶

In Python the scope of variables assigned to within a function is limited to that function. Their scope is "local" -- it is put in a dictionary accessed through the locals() function which is created on entry to the function and deleted on exit. This allows you to freely re-use names without worrying about conflicts with variables defined in other functions.

Variables that are not assigned to in a function refer to variables in the global scope accessed through globals() (or an enclosing function's scope).

It is possible to assign to a global variable by including it the global statement (typically avoided).

In the following example there are two scopes: the global one where x and y are bound to an integer with value 1 and the scope of the function f where a new variable x is created in the function's local namespace because it is assigned to. The global y statement binds y to the value in the enclosing scope. z exists only the function's namespace.

In this example the global variable x is "shadowed" by the local variable x.

In [2]:
x=1
y=1
print(x,y)

def f():
    global y
    x=2
    y=2
    z=2
    print(x,y,z)

f()
print(x,y)
print(globals()['x'])
print(z)
1 1
2 2 2
1 2
1
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [2], in <cell line: 15>()
     13 print(x,y)
     14 print(globals()['x'])
---> 15 print(z)

NameError: name 'z' is not defined

Module Namespaces¶

import statements also create namespaces. Variables and functions in the file that is imported are placed in a namespace that has the same name as the file. For example, when we import from the file math.py, the function cos and the variable pi are placed in the math namespace:

In [3]:
import math
type(math.cos), math.pi
Out[3]:
(builtin_function_or_method, 3.141592653589793)

We can also import all variables and functions defined in the imported file into the global namespace by using the from file import * syntax:

In [4]:
# import everything into the global namespace instead:
import math
print(globals().keys(),'math' in globals(),pi)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [4], in <cell line: 3>()
      1 # import everything into the global namespace instead:
      2 import math
----> 3 print(globals().keys(),'math' in globals(),pi)

NameError: name 'pi' is not defined

Closures¶

Each function has its own local namespace. Each time we create a new function we create a new namespace. This allows functionsn to store data. Such functions that hold data (state) are called "closures".

In this example the function newmin is a "factory" function that returns a function object. This function object keeps track of the smallest value it has been called with and returns it.

In Python we typically do this by defining a new type of object using the class keyword, but a closure has less overhead.

In [5]:
# a function that returns a function:
def newmin():   
    m=None
    def f(x):
        # 'nonlocal' binds m to the enclosing scope
        # instead of the global scope
        nonlocal m  
        if m == None or x<m:
            m=x
        return m
    return f

    
min1=newmin()
print(min1(15),min1(10),min1(20))

min2=newmin()
print(min2(10),min2(5),min2(15))

print(min1(100),min2(100))
15 10 10
10 5 5
10 5

Object Scope¶

Each instance of an object also has its own namespace:

In [6]:
class newclass():
    def __init__(self,x):
        self.x=x
    
c1=newclass(1)
c2=newclass(2)
x=3
c2.newthing = 4

print(c1.x,c2.x,x,c2.newthing)
1 2 3 4

Scope Resolution¶

A name can exists in multiple scopes. Names are searched for in the following order:

  1. local function scope
  2. object scope
  3. global scope
In [7]:
x=3

class myclass():
    # class variable shared by all instances of myclass:
    x=2
    def __init__(self,x):
        # instance variable (attribute) unique to an instance
        self.x=x
    def f1(self):
        # which x does it change?
        x=1
        return x
    def f2(self):
        return self.x
    def f3(self):
        # how is this different than f1?
        print(locals()['self'])
        return x
    


c=myclass(4)

# scopes: function, object, and global:
print(c.f1(), c.f2(), c.f3())

# global scope (in main namespace, unchanged)
print(x)
<__main__.myclass object at 0x0000024E087C5CA0>
1 4 3
3