Based on this version in Clojure
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))
x
is defined in the local scope of the functionfoo
y
is defined in the global scope
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
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.
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?
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?
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