Scopes

Dynamic vs Lexical Scope

An identifier such as variable name is "in scope" if it is bound 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".

Local Variables

In a Python function, we can create a local variable simply by assigning to it. The scope of variables assigned to within a function is limited to that function. Their scope is "local". 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 scope (global scope or an enclosing function's scope).

Variables assigned to outside a function are in the __main__ namespace and are "global". It is possible to refer to a global variable by using the global statement.

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 functions 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 [4]:
x=1
y=1

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

f()
print(x,y)
import __main__
'x' in dir(__main__), 'z' in dir(__main__)
2 2 2
1 2
Out[4]:
(True, False)

Module Namespaces

Importing a module also creates a namespace. 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 [1]:
import math
math.myfunction = 1
print(dir(math),math.myfunction)
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'myfunction', 'nan', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc'] 1
In [6]:
from math import *
import __main__
print(dir(__main__))
['In', 'Out', '_', '_4', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__main__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_i3', '_i4', '_i5', '_i6', '_ih', '_ii', '_iii', '_oh', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exit', 'exp', 'expm1', 'f', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'get_ipython', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'math', 'modf', 'myfunction', 'nan', 'pi', 'pow', 'quit', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'x', 'y']

The global namespace is in a module called __main__. Built-in identifiers exist the __builtin__ namespace in the global namespace:

In [7]:
import __main__
print(dir(__main__))
print(dir(__main__.__builtin__))
['In', 'Out', '_', '_4', '_5', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__main__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_i3', '_i4', '_i5', '_i6', '_i7', '_ih', '_ii', '_iii', '_oh', 'exit', 'f', 'get_ipython', 'math', 'quit', 'x', 'y']
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__IPYTHON__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display', 'divmod', 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

Object Scope

Each instance of an object also has its own scope.

In [10]:
class newclass():
    pass

c1=newclass()
c2=newclass()
c1.x=1
c2.x=2
x=3

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

Scope Resolution

A name can exists in multiple scopes. The priority is:

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

class myclass():
    x=2
    def __init__(self,x):
        self.x=x
    def f1(self):
        x=1
        return x
    def f2(self):
        return self.x
    def f3(self):
        return x
    


c=myclass(4)

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

# global scope (in main namespace)
print(x)

# module scope

import math
math.x=4
print(math.x)
1 4 3
3
4

Closures

Dynamic binding allows functions to retain state.

In this example we create a function object that returns the smallest value it has been called with since it was created.

We could accomplish the same thing by defining a new type of object with appropriate methods, but a closure has less overhead.

In [8]:
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