5.16. Scope¶
When Python looks up a name inside a function, it searches a specific sequence of scopes. Understanding that sequence explains why some assignments shadow outer names, why others modify them, and why nested functions can remember values from where they were defined.
Name lookup starts in the local function scope and walks outward to module and built-in scopes until a match is found.¶
5.16.1. Local and module scope¶
Names defined inside a function are local to that function and disappear when the call ends:
def f():
x = 10
print(x)
f()
print(x) # NameError: x is not defined
Names defined at the top level of a .py file are module-level
(sometimes called global) and are visible everywhere in that
file, including inside functions:
CAMERA = "OpenMV"
def banner():
print("running on", CAMERA)
Reading a module-level name from inside a function is automatic. Assigning to that name from inside a function is not – the assignment creates a new local variable that shadows the module-level one for the rest of the call:
counter = 0
def bump():
counter = counter + 1 # UnboundLocalError
The left-hand counter makes counter a local name in
bump, so the read on the right has no value to find.
5.16.1.1. The global keyword¶
To actually reassign a module-level name from inside a function,
declare it global first:
counter = 0
def bump():
global counter
counter += 1
Reach for global sparingly. Functions that mutate hidden
state are harder to reason about than functions that take values
in as arguments and return new values out. The usual fix for “I
need to share state” is to pass an object (a list, a dict, a class
instance) as an argument and mutate it instead.
5.16.2. Lambdas¶
A lambda builds a small anonymous function in a single
expression:
square = lambda x: x * x
square(7) # 49
It is exactly equivalent to
def square(x):
return x * x
The body of a lambda must be a single expression – no
statements, no multiple lines. The main use is passing a tiny
function as an argument to something that takes a function:
pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]
When the body grows past one expression, switch to a real def.
Naming a function with def also gives it a name in tracebacks,
which a lambda does not have.
5.16.3. Closures¶
A function defined inside another function can read names from the enclosing function’s scope. The inner function captures those names and keeps working even after the outer call has returned:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))
Output:
105 110
add5 and add10 are two separate functions, each
remembering its own n. A function that builds and returns
a customised inner function this way is called a closure. It is
the main reason a language needs nested functions in the first
place – a way to bake some state into a function value and then
hand that value off as a single callable.
Reading captured names happens automatically. Rebinding one needs an extra keyword. The example below does what looks right and fails:
def make_counter():
count = 0
def tick():
count = count + 1 # UnboundLocalError
return count
return tick
The assignment to count inside tick makes count local
to tick, the same way it would have made it local in a
top-level function. The nonlocal keyword tells Python “this
name lives in the enclosing function, rebind it there”:
def make_counter():
count = 0
def tick():
nonlocal count
count += 1
return count
return tick
c = make_counter()
print(c(), c(), c())
Output:
1 2 3
nonlocal is to enclosing-function scope what global is to
module scope. Note that mutating a captured object (calling
some_list.append(...), some_dict[k] = v) does not need
nonlocal – the name is not being rebound, only the object it
points at is being changed.