A Dive Into Python Closures and Decorators - Part 2

Published Aug 01, 2017Last updated Aug 14, 2017
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:
Screen Shot 2017-08-01 at 12.21.33 PM.png
Screen Shot 2017-08-01 at 12.16.30 PM.png

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
Discover and read more posts from Moyosore Sosan
get started
Enjoy this post?

Leave a like and comment for Moyosore

16
Be the first to share your opinion

Get curated posts in your inbox

Read more posts to become a better developer