A Dive Into Python Closures and Decorators - Part 2
In previous post, we talked about local functions, closures and quickly looked into basic decorator. In this post, we will round up on decorators.
Decorators
As mentioned previously, a decorator is itself a callable that takes in another callable and returns another callable i.e a function that takes in another function as argument and returns a function. Example:
def capitalize(func):
def uppercase():
result = func()
return result.upper()
return uppercase
@capitalize
def say_hello():
return "hello"
print(say_hello()) # 'HELLO'
@capitalize
is our decorator that takes in say_hello()
and returns uppercase
. Let's look at another example
def square(func):
def multiply(*args):
f = func(*args)
return f * f
return multiply
@square
def addition(x,y):
return x + y
print(addition(5,7)) # 144
print(addition(15,10)) # 625
The square decorator is applied to the addition function, what this means is that the result of calling our addition function will be passed into square as an argument. The resulting function from this is the multiply function, hence the result of addition is multiplied by itself.
Decorators with arguments
Remember a decorator is a callable function, this make it possible for us to pass in arguments to our decorators. Arguments are passed into decorators same way it can be passed into normal functions.
# Parsing arguments to a normal fuction
normal_function(arg1, arg2)
normal_function(arg1='Hello', arg2='Dear')
# Passing arguments into a decorator
@my_decorator(arg1, arg2)
def my_function():
pass
@my_decorator(arg1='Hello', arg2='World')
def my_function():
pass
Decorators can be written to take in both positional and keyword arguments just like any other function.
To make a decorator take in argument(s), it is included as part of the implementation for such decorator.
def decorator(arg1, arg2):
def main_decorator(func):
def func_wrapper(*args, **kwargs):
f = func(*args, **kwargs)
return 'Hi {0}, {1} {2}'.format(f, arg1, arg2)
return func_wrapper
return main_decorator
@decorator('hello', 'dear')
def my_function(name):
return name
print(my_function('Emma')) # 'Hi Emma, hello dear'
The decorator function is created with arguments and wrapped around the actual decorator main_decorator which performs the main function and then passes the result as a return value to decorator
Let us take a look at another example, this time with a keyword argument and output based on the argument passed.
def greet(time='day'):
print(time.lower())
def greet_decorator(func):
def greet_person(*args, **kwargs):
person = func(*args, **kwargs)
return "Good {0} {1}".format(time, person)
return greet_person
return greet_decorator
@greet()
def get_name(name):
return name
@greet('Morning')
def greet_name(name):
return name
@greet(time='Evening')
def name(name):
return name
print(get_name('Josh')) # Good day Josh
print(greet_name('Aduke')) # Good Morning Aduke
print(name('Chima')) # Good Evening Chima
As explained earlier, the greet decorators takes in argument and returns a result based on that.
Multiple decorators
A function can be decorated with more than one decorator. To do this, we stack the decorators on the functions. These decorators take turns to be applied on the function with the one directly ontop of the function being applied first
@decorator1
@decorator2
@decorator3
def my_function():
pass
From above example. decorator3 is first applied on my_function, followed by decorator2 and lastly decorator1.
An example:
def uppercase(func):
def wrap():
f = func()
return f.upper()
return wrap
def split_string(func):
def wrap():
f = func()
return f.split()
return wrap
@split_string
@uppercase
def hello():
return 'hello world'
print(hello()) # ['HELLO', 'WORLD']
The function hello() is decorated with two functions, uppercase and split_string. These two decorators are applied on the hello function one after the other.@uppercase is applied first, the result is then passed into @split_string
The hello function returns string hello word
, when @uppercase is applied, we have HELLO WORLD
as output. This output is then passed into @split_string which results in ['HELLO', 'WORLD']
.
If there were more decorators to the hello function, the chain continues to the last one.
functools.wraps
Python docstrings provide a convenient way of documenting Python modules, functions, classes, and methods.
In python, the help
function prints out associated docstring for a function. Tpying help(range)
in python repl provides the associated docstring for the python in-built function range as shown:
Functions have attrributes such as:
- __name__: The name of the function as defined
- __doc__: The docstring associated with the function
We can define docstrings for our functions
def doc_string():
"""
Docstrings help document python functions
This is the docstring associated with this function
"""
return "I have a docstring"
print (doc_string()) # 'I have a docstring'
print (doc_string.__name__) # doc_string
print (doc_string.__doc__)
# Docstrings help document python functions
# This is the docstring associated with this function
print (help(doc_string))
# Help on function doc_string:
# doc_string()
# Docstrings help document python functions
# This is the docstring associated with this function
Now, let's apply previously defined @uppercase on our doc_string function and print __name__ and __doc__ attribute for doc_string
@uppercase
def doc_string():
"""
Docstrings help document python functions
This is the docstring associated with this function
"""
return "I have a docstring"
print (doc_string()) # 'I HAVE A DOCSTRING'
print (doc_string.__name__) # wrap
print (doc_string.__doc__) # wrap()
print (help(doc_string))
# Help on function wrap:
# wrap()
We see we have a different __name__ and __doc__ atrribute for doc_string function. Notice it's taking up the attributes from the decorator function, this is because we have replaced doc_string function with the decorator function.
For the doc_string function to retain it's original attributes, we use the wraps
method in the python's functools
package. functools.wraps
helps us fix the metadata of the decorated function.
Now let's refactor our uppercase decorator to use functools.wraps
and apply it on doc_string()
import functools
def uppercase(func):
@functools.wraps(func)
def wrap():
f = func()
return f.upper()
return wrap
With @functools.wraps applied, our doc_string function can retain its attribute
@uppercase
def doc_string():
"""
Docstrings help document python functions
This is the docstring associated with this function
"""
return "I have a docstring"
print (doc_string()) # 'I have a docstring'
print (doc_string.__name__) # doc_string
print (doc_string.__doc__)
# Docstrings help document python functions
# This is the docstring associated with this function
print (help(doc_string))
# Help on function doc_string:
# doc_string()
# Docstrings help document python functions
# This is the docstring associated with this function
Why use decorators
- They are powerful and widely used in python
- They improve code clarity
- They reduce code complexity
Nice Post!
In the last code block,
“”"
print (doc_string()) # ‘I have a docstring’
“”"
should print upper case result.
Really great post