A Dive Into Python Closures and Decorators - Part 1
We will be looking at local functions, the concepts of closure and what decorators are, sounds like a lot? Then let's dive into it.
Local functions
Probably most functions you are familiar with are either defined in module/global scope or within classes i.e methods. However Python allows us to define our functions in a local scope i.e within a function.
Knowing this, local functions are functions defined within another function. We say these functions are defined to the scope of a specific function e.g
def my_function():
def my_local_function():
print('I am a local function, local to my_function')
print('I am not a local function')
The function my_local_function is a local function as it is only available inside my_function
and can only be used within the function.
Let's look at another example:
def remove_first_item(my_array):
def get_first_item(s):
return s[0]
my_array.remove(get_first_item(my_array))
return my_array
print(remove_first_item(['1','2','3']))) # ['2','3']
print(get_first_item([1,2,3,4]))
# NameError: name 'get_first_item' is not defined
From the above, we see that calling get_first_item throws an error. This is because it can only be accessed within the remove_first_item function, that makes it a local function.
Local functions can also be returned from functions. Returning a local function is similar to returning in any other object. Let's take a look:
def enclosing_func():
def local_func():
print ('I am a local function')
return local_func()
print(enclosing_func()) # I am a local function'
Local functions are subject to the same scoping rules as other functions, this brings to the LEGB rule
for name look up in python - checking starts with the local scope, then the enclosing, then global and finally the built-in scope
LEGB - Local, Eclosing, Global, Built-in
x = 'global'
def outer_func():
y = 'enclose'
def inner_func():
z = 'local'
print(x, y, z)
inner_func()
print(outer_func()) # global enclose local
Local functions can be used when there is need for specialized functions. They also help with code organization and readability.
Closure
The local functions we have looked at so far have no definite way of interacting with the enclosing scope, that is about to change. Local functions can make use of variables in their enclosing scope, the LEGB rule
makes this possible.
def outer_func():
x = 5
def inner_func(y = 3):
return (x + y)
return inner_func
a = outer_func()
print(a()) # 8
From the above, we see that the inner_func function makes reference to the outer_func fucntion for the value of x. The local function is able to reference the outer scope through closures
. Closures maintain references to objects from the earlier scope.
Closure is commonly used in what is referred to as Function Factory - these are functions that return other functions. The returned functions are specialized. The Function Factory takes in argument(s), creates local function that creates its own argument(s) and also uses the argument(s) passed to the function factory. This is possible with closures
def multiply_by(num):
def multiply_by_num(k):
return num * k
return multiply_by_num
five = multiply_by(5)
print(five(2)) # 10
print(five(4)) # 20
decimal = multiply_by(10)
print(decimal(20)) # 200
print(decimal(3)) # 30
Here we see that the local function multiply_by_num takes an argument k
and returns the argument multiplied by the argument of it's enclosing function. Also, multply_by fuction takes an argument num
and returns the function that multiplies num
by its argument.
LEGB rule does not apply when new name bindings happen
text = "global text"
def outer_func():
text = "enclosing text"
def inner_func():
text = "inner text"
print('inner_func:', text) # inner_func: global text
print('outer_func:', text) # outer_func: enclosing text
inner_func()
print('outer_func:', text) # outer_func: enclosing text
print('global:', text) # global: global text
outer_func()
print('global:', text) # global: global text
In the inner_func function, we have created a new name binding by re-assiging the variable text
in the function scope. Calling the inner_func does not affect the variable text
in the outer_func, likewise calling outer_func does not affect the global variable text
.
global
global
is a python keyword that introduces names from global namespace into the local namespace. From the previous code, we can make the inner_func to modify the variable text
rather than create a new one.
text = "global text"
def outer_func():
text = "enclosing text"
def inner_func():
global text # binds the global text to the local text
text = "inner text"
print('inner_func:', text) # inner_func: inner text
print('outer_func:', text) # outer_func: enclosing text
inner_func()
print('outer_func:', text) # outer_func: enclosing text
print('global:', text) # global: global text
outer_func()
print('global:', text) # global: inner text
Since the global
keyword has binded the global text
variable to the local text
variabel, calling the outer_func function makes changes to the global text, hence, reassigning the variable text
in the global scope.
nonlocal
The nonlocal
keyword allows us to introduce names from enclosing namespace into the local namespace. Still looking at the previous code:
text = "global text"
def outer_func():
text = "enclosing text"
def inner_func():
nonlocal text # binds the local text to the enclosing text
text = "inner text"
print('inner_func:', text) # inner_func: inner text
print('outer_func:', text) # outer_func: enclosing text
inner_func()
print('outer_func:', text) # outer_func: inner text
print('global:', text) # global: global text
outer_func()
print('global:', text) # global: global text
Here, the enclosing text
variable changes when the inner_func was called.
Decorators
Since we have understanding of local functions and closure, we can then look at function decorators.
Decorators are used to enhance existing functions without changing their definition. A decorator is itself a callable and takes in another callable to return another callable. Simply put - A decorator is a function that takes in another function and returns another function, although it is a bit more than just that.
# Syntax for decorator
@my_decorator
def my_function():
pass
The result of calling my_function is passed into the my_decorator
def capitalize(func):
def uppercase():
result = func()
return result.upper()
return uppercase
@capitalize
def say_hello():
return "hello"
print(say_hello()) # 'HELLO'
The result of calling say_hello is passed into the capitalize decorator. The decorator modifies the say_hello function by changing its result to uppercase. We see that capitalize decorator takes in a callable(say_hello) as an argument and returns another callable(uppercase). This is just a basic example on decorator
In the next post, we will continue with understanding how decorators work in python. In preparation for that, let's take a brief look at *args and **kwargs
*args and **kwargs
*args
allows you to use any number of arguments in a function. You use it when you're not sure of how many arguments might be passed into a function
def add_all_arguments(*args):
result = 0
for i in args:
result += i
return result
print(add_all_arguments(1,5,7,9,10)) # 32
print(add_all_arguments(1,9)) # 10
print(add_all_arguments(1,2,3,4,5,6,7,8,9,10)) # 55
print(add_all_arguments(1)) # 1
print(add_all_arguments()) # 0
Like *args
, **kwargs
can take many arguments you would like to supply to it. However, **kwargs
differs from *args
in that you will need to assign keywords
def print_arguments(**kwargs):
print(kwargs)
print(print_arguments(name = 'Moyosore')) # {'name': 'moyosore'}
print(print_arguments(name = 'Moyosore'), country = 'Nigeria')
print(print_arguments()) # {}
# {'name': 'moyosore', 'country': 'Nigeria'}
def print_argument_values(**kwargs):
for key, value in kwargs.items():
print('{0}: {1}'.format(key, value))
print_argument_values(name="Moyosore", country="Nigeria")
# name: Moyosore
# country: Nigeria
Args and kwargs can be used together in a function, with args always coming before kwargs. If there are any other required arguments, they come before args and kwargs
def add_and_mul(*args, **kwargs):
pass
def add_and_mul(my_arg, *args, **kwargs):
pass
def add_and_mul(my_arg, my_arg_1, *args, **kwargs):
pass
You can do more reading on args and kwargs for better understanding. This is just an example for you to have basic idea on args and kwargs.
Next post will be dealing with decorators in details!
Just a quick correction. Your example for
nonlocal
has 2 errors in it. It should read:Thanks, that was an oversigth on my end. Corrected