Skip to content

Instantly share code, notes, and snippets.

@astrieanna
Created December 22, 2021 18:32
Show Gist options
  • Save astrieanna/b11eea16e92df80a6af0052cfb3e9bd2 to your computer and use it in GitHub Desktop.
Save astrieanna/b11eea16e92df80a6af0052cfb3e9bd2 to your computer and use it in GitHub Desktop.
A series of exercises to develop an intuition for how scoping works in Python

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))
  • x is defined in the local scope of the function foo
  • y is 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?

Function Closure

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

Exercises

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment