def add_ten_to_things():
# inside this function is a scope
# we'll call it "outer scope"
= 10
x def inner(y):
# this function has a scope
# we'll call it "inner scope"
return x + y
return inner
Closures in Python: What they are, and a practical application for type-checking objects
A closure is technique that allows functions to bundle variables in their local scope for access at a later time. If there was a variable x with the value of ten in the namespace for instance, even after that code executed, we could refer back to the namespace and x would still be 10. That’s a little dry; fortunately the idea is easier to illustrate in code using a function with a function inside it.
Notice how in the inner function we refer to the outer scope (to the variable x) and then return the inner scope? This is the mechanism that allows closures to work: that x is now packaged into the returned inner(). Let’s see how it can be accessed:
# we call the original add_ten_to_things() function that returns the inner function/namespace
= add_ten_to_things()
add_ten
# we pass something for "y" and it will still be able to add 10 to it
print(add_ten(20))
30
Because 10 is stored in the local namespace of the outer function that produces the new function, 10 can still be accessed by it. A function that makes functions that add ten to things isn’t especially useful, however:
def add_x_to_things(x):
def adder(y):
return x + y
return adder
= add_x_to_things(5)
increase_by_five = add_x_to_things(10)
increase_by_ten
= increase_by_five(1)
one_plus_five = increase_by_ten(6)
one_plus_ten
print(one_plus_five, one_plus_ten)
6 16
You’d be forgiven for wondering what the point of that would be. It makes more sense once you realize you can pass arguments to the outer function and that they will be accessible to the inner one. To illustrate, consider the following example, where a function can make several types of greeter builders depending on what is passed to the outer function.
# the outer function
def greeter_maker(greeting):
# inner function
def greeter(name):
print(greeting + ",", name)
# outer function returns the inner
return greeter
= greeter_maker("Hello, pleased to meet you")
formal_greeter = greeter_maker("Heyo") casual_greeter
In action:
"Josh")
casual_greeter("Sruthi") formal_greeter(
Heyo, Josh
Hello, pleased to meet you, Sruthi
Another builder:
While this show some flexibility that could be made into something useful, it can be hard to see just what a powerful idea this idea is without some context, so we will look at one more example. Consider the following class:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
This is an extremely simple Python class that models a Person with a name and age. It’s probably obvious to any hypothetical user of this class that “name” is a string and “age’ is an integer (or at least a number or some kind). However, as Python is dynamically typed, there is nothing in the language itself to enforce this. Consider the following:
# everything goes according to plan...
= Person("Christian Slater", 49)
christian
# whoops, wrong order...
= Person(37, "Rami Malek")
rami
# gets nane
print(christian.name)
# not what we though we'd get
print(rami.name)
Christian Slater
37
This program will run just fine until sometime downstream when someone tries to do a computation a involving rami’s name or age but has the wrong type of data (ie, age += 1 and finds it’s a string). Consider the following usage of a closure to prevent this (this is much simpler than it seems once you see it in context so hang on):
# takes a dictionary of attributes->types, and a model
def typechecking_builder(required_params, model):
def _builder(attributes_passed):
# iterate through the dict that was passed to the outer function
for k in attributes_passed.keys():
= attributes_passed[k]
current_param = required_params[k]
type_required # ensure they're what we've been told to expect
try:
assert type(current_param) == type_required
print("Type check passed for", current_param, "of type", type_required)
# raise an error if they're not
except AssertionError:
print("\tfound", current_param, "of type", type(current_param), "Required:", type_required)
raise TypeError("Type check failed for:", k) # whatever you want to do for exception handling
# return the type checking function
return _builder
We can now make a builder that will type check for us, telling the function what it should expect when it is called:
# make people, thier name is a string, thier age is an int, they're of type Person
= typechecking_builder({"name": str, "age":int}, Person) person_builder
When we use this, it will check the types we passed:
= person_builder({"name": "Carly Chaikin", "age": 28}) carly
Type check passed for Carly Chaikin of type <class 'str'>
Type check passed for 28 of type <class 'int'>
Now, if we make the simple mistake from before, we get feedback right away:
= person_builder({"name": 32, "age": "Sunita Mani"}) sunita
found 32 of type <class 'int'> Required: <class 'str'>
TypeError: ('Type check failed for:', 'name')
…and every Person we build will be type checked!