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 [2]:
a=1
print(a)
del(a)
print(a)
1
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 4
      2 print(a)
      3 del(a)
----> 4 print(a)

NameError: name 'a' is not defined

locals() and globals() functions return dictionaries of variables in the local and global namespaces. Although not recommended, we can manipulate these dictiionaries and add/access/remove variables to show how namespaces are implemented:

In [3]:
locals()['b'+str(3)]=4
print(b3)
globals()['c'+'d']=5
print(cd)
del(locals()['b3'])
print(b3)
4
5
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 6
      4 print(cd)
      5 del(locals()['b3'])
----> 6 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" -- the variables are put in a dictionary which returned by the locals() function. This dictionary 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 an enclosing function's scope, or in the global (top-level) scope. The global scope can be accessed through a dictionary returned by the globals() function.

It is possible to assign to a global variable by including it in a global statement.

It is possible to assign to a variable in the enclosing function's scope by including it in a nonlocal statement.

In the following example there are three scopes:

  • the global scope where x and y are bound to an integer with value 1
  • the local scope of the function f where a new variable x and z are created in the function's local namespace because they are assigned to, and y refers to y in the global namespaces because of the global statement
  • the local scope of the function g where the variable x from the function f scope (not the global x) is made accessible by a nonlocal statement.

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

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

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

f()
print(x,y)
print(globals()['x'])
print(z)
1 1
2 2 2
3 2 2
1 2
1
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 20
     18 print(x,y)
     19 print(globals()['x'])
---> 20 print(z)

NameError: name 'z' is not defined

If an explicit namespace is not given, names are searched for in the following order:

  1. local function scope
  2. enclosing function scope
  3. global scope
  4. built-in functions

(the acronym is LEGB).

Module Namespaces and Packages¶

Python code that is meant to be re-used (a "library") is placed in modules. Modules can be grouped into packages. In many cases:

  • a module is a file with a .py extension
  • a package is a directory that contains a __init__.py file (which is thus also a module)

The statement importfile searches in the list of directories sys.path for a file named file.py (a module) or a directory named file containing a file named __init__.py (a package). The .py files are executed and any objects created are put in a "module" namespace named file.

__init__.py is often empty.

Modules and packages can be placed in a package. For example, a import encoders.video statement would execute encoders/video.py (a module) or encoders/video/__init__.py (a package) in the codecs.video module namespace (assuming encoders/__init__.py existed).

The above is a simplified description; modules can be found and loaded in other ways.

The example below shows a package named encoders containing a module named audio and a package named video:

encoders\
  __init__.py
  audio.py
  video\
    __init__.py

The import statements execute the corresponding .py files which in this example only add variables to the module namespace. However, modules typically define related types, functions, and constants using class, def and assignment statements.

In [1]:
# the directory structure and contents
!dir /s encoders
!type encoders\__init__.py
!type encoders\video\__init__.py
!type encoders\audio.py

import encoders
print(encoders,dir(encoders),sep='\n')
import encoders.video
print(encoders.video,dir(encoders.video),sep='\n')
import encoders.audio
print(encoders.audio,dir(encoders.audio),sep='\n')
print(dir(encoders),sep='\n')
 Volume in drive C is Windows
 Volume Serial Number is 1A85-7D8B

 Directory of C:\Users\Ed\SharedFolder\bcit\4653\notebooks\lectures\encoders

2023-05-16  07:54 AM    <DIR>          .
2023-05-16  07:54 AM    <DIR>          ..
2023-05-15  10:10 PM                 4 audio.py
2023-05-16  07:54 AM    <DIR>          video
2023-05-15  10:10 PM                 4 __init__.py
               2 File(s)              8 bytes

 Directory of C:\Users\Ed\SharedFolder\bcit\4653\notebooks\lectures\encoders\video

2023-05-16  07:54 AM    <DIR>          .
2023-05-16  07:54 AM    <DIR>          ..
2023-05-15  10:20 PM                 4 __init__.py
               1 File(s)              4 bytes

     Total Files Listed:
               3 File(s)             12 bytes
               5 Dir(s)  293,570,883,584 bytes free
a=1
b=1
c=1
<module 'encoders' from 'C:\\Users\\Ed\\SharedFolder\\bcit\\4653\\notebooks\\lectures\\encoders\\__init__.py'>
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'a']
<module 'encoders.video' from 'C:\\Users\\Ed\\SharedFolder\\bcit\\4653\\notebooks\\lectures\\encoders\\video\\__init__.py'>
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'b']
<module 'encoders.audio' from 'C:\\Users\\Ed\\SharedFolder\\bcit\\4653\\notebooks\\lectures\\encoders\\audio.py'>
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'c']
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'a', 'audio', 'video']

We can also import one or all variables in a module into the global namespace by using the from file importname or from file import * syntax:

In [9]:
# iimport the cos function into the global namespace:
from math import cos
# import everything into the global namespace (not typically done):
from math import *
#import math
print(globals().keys())
print('math' in globals())
print(pi)
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__builtin__', '__builtins__', '_ih', '_oh', '_dh', 'In', 'Out', 'get_ipython', 'exit', 'quit', 'open', '_', '__', '___', '_i', '_ii', '_iii', '_i1', '_exit_code', 'encoders', '_i2', '_i3', 'cd', '_i4', 'x', 'y', 'f', '_i5', 'newmin', 'min1', 'min2', '_i6', 'cos', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cosh', 'degrees', 'dist', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log1p', 'log10', 'log2', 'modf', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', 'prod', 'perm', 'comb', 'nextafter', 'ulp', 'pi', 'e', 'tau', 'inf', 'nan', '_i7', '_i8', 'math', '_i9'])
True
3.141592653589793

Closures¶

Each function has its own local namespace. Each time we create a new function we create a new namespace. This allows functions 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 closures have 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 [3]:
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)
print(c1.newthing)
1 2 3 4
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 11
      8 c2.newthing = 4
     10 print(c1.x,c2.x,x,c2.newthing)
---> 11 print(c1.newthing)

AttributeError: 'newclass' object has no attribute 'newthing'