The functions locals() and globals() can be used to view these for debugging purposes.
Modifying a global variable within a function
It is possible for a function to modify a global mutable:
d = {"key": 123}def f(): d["key"] =456f()# changes to d persist outside function, mutability!print(d)
{'key': 456}
Despite being possible, this is generally a bad idea, mutability is why using globals makes programs hard to follow.
What if we instead try to change an immutable via reassignment?
s ="hello "def f(): s +=" world!"# attempt to both use & reassign stry: f()exceptExceptionas e:print(repr(e))
UnboundLocalError("cannot access local variable 's' where it is not associated with a value")
global
Very very occasionally, we want to allow for this:
s ="hello "def f():# indicates that the name s within the function refers to the global sglobal s s +=" world!"# attempt to both use & reassign stry: f()print("s after function:", s)exceptExceptionas e:print(repr(e))
s after function: hello world!
This is a bad idea for the same reason that modifying the mutable was, tracking changes to global state makes it very hard to reason about functions.
# this seems like a reasonably safe use of globalsNUM_ENTRIES =1000SECRET_KEY ="1899232-32kldfj3"IGNORED_KEYS = ["middle_name", "old_badge_num"]# looking at this code, one assumes that it'd be possible to reorder/parallelize# clean_data_part1 and part2, but if the author of these functions for some# reason used `global`, or modifies the list IGNORED_KEYS, there are hard-to-observe# actions that can lead to subtle bugsdata = read_data()data = clean_data_part1(data)data = clean_data_part2(data)publish_data(data)
builtins
Built-in scope is above global scope, if a variable does not exist at the global/module level, a final scope is searched.
When you use functions like print(), dict(), or map(), those are from the builtin scope:
for name indir(__builtins__):if name[0].islower():print(name)
abs
aiter
all
anext
any
ascii
bin
bool
breakpoint
bytearray
bytes
callable
chr
classmethod
compile
complex
copyright
credits
delattr
dict
dir
display
divmod
enumerate
eval
exec
execfile
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
runfile
set
setattr
slice
sorted
staticmethod
str
sum
super
tuple
type
vars
zip
Enclosing Scope
Remember, our scope lookup order is LEGB: local, enclosing, global, built-in.
Enclosing scope exists when we have nested functions:
# here we see that f2 can access f1's y, and that there# are two distinct x variables, inside f1 and f2def f1(): x ="OUTER" y ="from enclosing"def f2(): x ="INNER"print("y=", y)print("inside f2 x=", x)print("inside f1 before f2 has been called x=", x) f2()print("inside f1 after f2 has been called", x)f1()
inside f1 before f2 has been called x= OUTER
y= from enclosing
inside f2 x= INNER
inside f1 after f2 has been called OUTER
Closures
When a function is nested inside another function, future calls can continue to access the original enclosing scope.
The combination of a nested function and its enclosing scope is called a closure.
def make_func(n):def f(x):# n: locally scoped to make_func() < enclosing scope# x: locally scoped to f()# we are using the n from the enclosing scopereturn x ** n return f
to_the_third = make_func(3)to_the_third(10) # remembers the n from the enclosing scoped
1000
squared = make_func(2)squared(10) # has it's own distinct n from it's closure
100
We can use this to create cache behavior:
import mathdef make_cached_calc(): prior_calls = {}def calc(x, y):# this portion only runs if we haven't seen x, y beforeif (x, y) notin prior_calls:print(f"doing computation on {x} and {y}...")# do 'expensive' computation answer = math.sin(x) + math.exp(y)# save to cache prior_calls[x, y] = answer# always runsprint("cache=", prior_calls)# retrieve from cache, will be present by this pointreturn prior_calls[x, y]# return function w/ enclosing scope of prior_calls return calc# obtain inner functiondo_computation = make_cached_calc()
do_computation(1, 2)
doing computation on 1 and 2...
cache= {(1, 2): 8.230527083738547}
8.230527083738547
do_computation(1, 2)
cache= {(1, 2): 8.230527083738547}
8.230527083738547
do_computation(0.5, 0.7)
doing computation on 0.5 and 0.7...
cache= {(1, 2): 8.230527083738547, (0.5, 0.7): 2.4931782460746796}
2.4931782460746796
# has own enclosuredo_computation2 = make_cached_calc()do_computation2(1, 2)
doing computation on 1 and 2...
cache= {(1, 2): 8.230527083738547}
8.230527083738547
nonlocal
There is also a nonlocal, which allows modifying a variable declared in enclosing scope.
Like global, it is placed inside the inner function, allowing access to an enclosing variable (not vice-versa!).
def create_counter_func(): counter =0def f():# gives us permission to access the enclosing counternonlocal counter counter +=1print(f"called {counter} times")return fg = create_counter_func()h = create_counter_func()