Introduction to Closures
Learn how to return functions from other functions and use this technique to create closures. Discover what closures are and how they can be used to create function factories and a kind of 'protected memory'.
Let's begin with a simple example of a function nested inside another function.
def outer_function(message):
def inner_function():
print(message)
inner_function()
outer_function('Hello')
outer_function
takes the message
paramter and defines a nested function called inner_function
, which it immediately executes.
The inner_function
prints the value of message
, which it can access through the enclosing scope of outer_function
.
I know it seems a bit overly complicated to use all this code just to print a simple message.
Things get interesting when the outer_function
doesn't just execute the inner_function
, but instead returns it:
def outer_function(message):
def inner_function():
print(message)
#### returning the nested function ####
return inner_function
Now we can assign the nested function to a new variable and execute it outside outer_function
:
def outer_function(message):
def inner_function():
print(message)
#### returning the nested function ####
return inner_function
#### Storing the nested function in a new variable
greet = outer_function('Hello')
#### Calling the nested function ####
greet()
In this way, you could say that the nested function has "escaped" the outer_function
.
The key takeaway is that it still retains access to the original value of message
from its enclosing scope even though outer_function
has finished executing.
This is what's called a closure.
A closure is created when a nested function "remembers" the variables from its enclosing scope even after the outer function has finished executing.
This means the inner function can still access and use those variables.
In our example, we can now create different versions of the inner_function
by passing different values to the outer_function
:
def outer_function(message):
def inner_function():
print(message)
return inner_function
#### create different versions of inner_function ####
greet = outer_function('Hello')
farewell = outer_function('Goodbye')
greet()
farewell()
Now, we've used the outer_function
as a function factory.
A function factory is a function that produces other functions, allowing you to create specialized behavior based on input parameters.
What will be the output?
def outer(x):
y = 5
def inner():
print(x * y)
return inner
func = outer(100)
func()
What will be the output?
def outer(x):
y = 5
def inner():
x = 10
print(x * y)
return inner
func = outer(100)
func()
Function factories are just one of many use cases for closures.
You can also use closures to manage a state that you want to keep hidden from the outside world.
For example, here we create a create_counter
function that manages the state of a count
variable:
def create_counter():
count = 0 # Protected state
def increment():
nonlocal count # Access the enclosing function's variable
count += 1
print(count)
return increment
my_counter = create_counter()
my_counter()
my_counter()
my_counter()
The nonlocal
keyword allows the inner function to modify the count
variable from the enclosing create_counter
function.
The moment we execute the create_counter
function, we create a new instance of the count
variable.
This value is not directly accessible to us. It can only be modified by the nested function increment
.
However, this nested function is available to use because the outer function returned it, and we assigned it to the my_counter
variable.
Be executing my_counter
, we can indirectly modify the count
.
What will be the output?
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
print(count)
return increment
func = create_counter()
print(count)
What will be the output?
def create_counter():
count = 0
def increment():
nonlocal count
count += 2
print(count)
return increment
func = create_counter()
func()
func()
func()
What will be the output?
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
print(count)
return increment
func1 = create_counter()
func2 = create_counter()
func1()
func2()
func1()
func2()
What will be the output?
def create_counter(step):
count = 0
def increment():
nonlocal count
count += step
print(count)
return increment
func = create_counter(5)
func()
func()
What will be the output?
def create_counter(step):
count = 0
def increment():
nonlocal count
count += step
print(count)
return increment
func1 = create_counter(5)
func2 = create_counter(2)
func1()
func1()
func2()
func2()
What will be the output?
def create_counter():
count = 0
def increment(step):
nonlocal count
count += step
print(count)
return increment
func = create_counter()
func(1)
func(2)
func(3)