Based on this version in Clojure
Lexical Scope in Python
Lexical scope guarantees that the reference to a value will be "enclosed" in the scope in which it is being used.
Lexical scoping simplifies our life because it allows us to mechanically follow code and determine where a value originated.
- Start at the place of reference of the value
- Then "walk" backwards and outwards to find where the value was defined
- Now you know where the value came from
This also helps reduce our mental burden of inventing new names to refer to things because we can re-use a name within a limited scope, and be certain that it will not destroy anything with the same name outside the given scope.
There are three kinds of scopes in Python: global, local, and class. I'm going to focus on global and local scopes here, and we can consider classes later.
- Global scope is defining variables at the top-level; this is anything not contained in a function or class
- Local scope is defining variables within a function
- Class scope is defining variables within a class
In this code:
def foo(x): return x + 2 if __name__ == '__main__': y = 5 print(foo(y))
xis defined in the local scope of the function
yis defined in the global scope
Exercise Set 1
Develop an intuition for what "lexical scope" might mean by reasoning about the following exercises. Mentally evaluate and predict the results, then check.
# These statements are intended to be entered sequentially at the REPL x = 42 # define a variable 'x' and set the value to 42, globally x # obviously returns 42 (lambda x: x)(x) # also returns 42, but how? # to avoid lambdas, the above could also be written: def foo(x): return x foo(x) def bar(): x = 10 return x # returns the local-scope x bar() x # returns the global-scope x bar() + x # returns 52
Exercise Set 2
Read carefully and compare these three function variants.
Restart the REPL here when checking your answers, just to have a clean slate.
x = 42 def add_one_v1(x): return x + 1 # which 'x' will this 'x' reference? add_one_v1(1) # should evaluate to what? add_one_v1(x) # should evaluate to what? def add_one_v2(z): return x + 1 # which 'x' will this 'x' reference? add_one_v2(1) # should evaluate to what? add_one_v2(x) # should evaluate to what? def add_one_v3(x): x = 10 return x + 1 # which 'x' will this 'x' reference? add_one_v3(1) # should evaluate to what? add_one_v3(x) # should evaluate to what? x # what value does the global 'x' now have?
This is a way for a function to capture and "close over" any value available at the time the function is defined.
In order to make this work with Python local scopes, which only occur within functions, I am defining
get_scale_by_PI to be a function that returns another function. I realize this is weird compared to how Python is normally written.
Version with a Closure
Start from a new REPL for this.
def get_scale_by_PI(): pi = 3.14159 return lambda x: x * pi scale_func = get_scale_by_PI() scale_func(2) # what will this evaluate to? pi = 5 scale_func(2) # what will this evaluate to? x = 10 scale_func(2) # what will this evaluate to?
Version using global variable
Start with a new REPL for this.
pi = 3.14159 def get_scale_by_PI(): return lambda x: x * pi scale_func = get_scale_by_PI() scale_func(2) # what will this evaluate to? pi = 5 scale_func(2) # what will this evaluate to? x = 2 scale_func(2) # what will this evaluate to?
A More General scale-by
Use this function definition to define some other functions below
# Given a number x, # return a function that accepts another number y, # and scales y by x def scale_by(x): return lambda y: x * y # whatever is passed by x is captured # within the body of the returned function
Define a few scaling functions in terms of
scale-by given above.
# Returns x scaled by PI def scale_by_PI_v2(x): pass # Returns 4x the given number def quadruple(x): pass # Returns half the given number def halve(x): pass