Beginners Guide to Python 3 – Python Decorators

Python Decorators

Sometimes you want to modify an existing function without changing its source code. A common example is adding extra processing (e.g. logging, timing, etc.) to the function.

That’s where decorators come in.

A decorator is a function that accepts a function as input and returns a new function as output, allowing you to extend the behavior of the function without explicitly modifying it.

Every decorator is a form of metaprogramming.

Metaprogramming is about creating functions and classes whose main goal is to manipulate code (e.g., modifying, generating, or wrapping existing code).

Python Functions

Before you can understand decorators, you must first know how functions work.

Pass Function as Arguments

In Python, functions are first-class objects. This means that they can be passed as arguments, just like any other object (string, int, float, list etc.).

If you have used functions like map or filter before, then you already know about it.

Consider the following example.

def hello1():
    print("Hello World")

def hello2():
    print("Hello Universe")

def greet(func):
    func()

greet(hello1)
# Prints Hello World
greet(hello2)
# Prints Hello Universe

Here, hello1() and hello2() are two regular functions and are passed to the greet() function.

Note that these functions are passed without parentheses. This means that you are just passing their reference to greet() function.

Inner Functions

Functions can be defined inside other functions. Such functions are called Inner functions. Here’s an example of a function with an inner function.

def outer_func():
    def inner_func():
        print("Running inner")
    inner_func()

outer_func()
# Prints Running inner

Returning a Function

Python also allows you to return a function. Here is an example.

def greet():
    def hello(name):
        print("Hello", name)
    return hello

greet_user = greet()

greet_user("Bob")
# Prints Hello Bob

Again, the hello() function is returned without parentheses. This means that you are just returning its reference.

And this reference is assigned to greet_user, due to which you can call greet_user as if it were a regular function.

Simple Decorator

Now that you’ve learned that functions are just like any other object in Python, you are now ready to learn decorators.

Let’s see how decorators can be created in Python. Here is a simple example decorator that does not modify the function it decorates, but rather prints a message before and after the function call.

def decorate_it(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

def hello():
    print("Hello world")

hello = decorate_it(hello)

hello()
# Prints Before function call
# Prints Hello world
# Prints After function call

Whatever function you pass to decorate_it(), you get a new function that includes the extra statements that decorate_it() adds. A decorator doesn’t actually have to run any code from func, but decorate_it() calls func part way through so that you get the results of func as well as all the extras.

Simply put, a decorator is a function that takes a function as input, modifies its behavior and returns it.

The so-called decoration happens when you call the decorator and pass the name of the function as an argument.

hello = decorate_it(hello)

Here you applied the decorator manually.

Syntactic Sugar

As an alternative to the manual decorator assignment above, just add @decorator_name before the function that you want to decorate.

The following example does the exact same thing as the first decorator example:

def decorate_it(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorate_it
def hello():
    print("Hello world")

hello()
# Prints Before function call
# Prints Hello world
# Prints After function call

So, @decorate_it is just an easier way of saying hello = decorate_it(hello).

Decorating Functions that Takes Arguments

Let’s say you have a function hello() that accepts an argument and you want to decorate it.

def decorate_it(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorate_it
def hello(name):
    print("Hello", name)

hello("Bob")
# Prints _wrapper() takes 0 positional arguments but 1 was given

Unfortunately, running this code raises an error. Because, the inner function wrapper() does not take any arguments, but we passed one argument.

The solution is to include *args and **kwargs in the inner wrapper function. The use of *args and **kwargs is there to make sure that any number of input arguments can be accepted.

Let’s rewrite the above example.

def decorate_it(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        func(*args, **kwargs)
        print("After function call")
    return wrapper

@decorate_it
def hello(name):
    print("Hello", name)

hello("Bob")
# Prints Before function call
# Prints Hello Bob
# Prints After function call

Returning Values from Decorated Functions

What if the function you are decorating returns a value? Let’s try that quickly:

def decorate_it(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        func(*args, **kwargs)
        print("After function call")
    return wrapper

@decorate_it
def hello(name):
    return "Hello " + name

result = hello("Bob")

print(result)
# Prints Before function call
# Prints After function call
# Prints None

Because the decorate_it() doesn’t explicitly return a value, the call hello("Bob") ended up returning None.

To fix this, you need to make sure the wrapper function returns the return value of the inner function.

Let’s rewrite the above example.

def decorate_it(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@decorate_it
def hello(name):
    return "Hello " + name

result = hello("Bob")

print(result)
# Prints Before function call
# Prints After function call
# Prints Hello Bob

Preserving Function Metadata

Copying decorator metadata is an important part of writing decorators.

When you apply a decorator to a function, important metadata such as the name, doc string, annotations, and calling signature are lost.

For example, the metadata in our example would look like this:

def decorate_it(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorate_it
def hello():
    '''function that greets'''
    print("Hello world")

print(hello.__name__)
# Prints wrapper
print(hello.__doc__)
# Prints None
print(hello)
# Prints <function decorate_it.<locals>.wrapper at 0x02E15078>

To fix this, apply the @wraps decorator from the functools library to the underlying wrapper function.

from functools import wraps

def decorate_it(func):
    @wraps(func)
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorate_it
def hello():
    '''function that greets'''
    print("Hello world")

print(hello.__name__)
# Prints hello
print(hello.__doc__)
# Prints function that greets
print(hello)
# Prints <function hello at 0x02DC5BB8>

Whenever you define a decorator, do not forget to use @wraps, otherwise the decorated function will lose all sorts of useful information.

Unwrapping a Decorator

Even if you’ve applied a decorator to a function, you sometimes need to gain access to the original unwrapped function, especially for debugging or introspection.

Assuming that the decorator has been implemented using @wraps, you can usually gain access to the original function by accessing the __wrapped__ attribute.

For example,

from functools import wraps

def decorate_it(func):
    @wraps(func)
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorate_it
def hello():
    print("Hello world")

original_hello = hello.__wrapped__

original_hello()
# Prints Hello world

Nesting Decorators

You can have more than one decorator for a function. To demonstrate this let’s write two decorators:

  • double_it() that doubles the result
  • square_it() that squares the result
from functools import wraps

def double_it(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result * 2
    return wrapper

def square_it(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result * result
    return wrapper

You can apply several decorators to a function by stacking them on top of each other.

@double_it
@square_it
def add(a,b):
    return a + b

print(add(2,3))
# Prints 50

Here, the addition of 2 and 3 was squared first then doubled. So you got the result 50.

result = ((2+3)^2)*2
       = (5^2)*2
       = 25*2
       = 50

Execution order of decorators

The decorator closest to the function (just above the def) runs first and then the one above it.

Let’s try reversing the decorator order:

@square_it
@double_it
def add(a,b):
    return a + b

print(add(2,3))
# Prints 100

Here, the addition of 2 and 3 was doubled first then squared. So you got the result 100.

result = ((2+3)*2)^2
       = (5*2)^2
       = 10^2
       = 100

Applying Decorators to Built-in Functions

You can apply decorators not only to the custom functions but also to the built-in functions.

The following example applies the double_it() decorator to the built-in sum() function.

from functools import wraps

def double_it(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result * 2
    return wrapper

double_the_sum = double_it(sum)

print(double_the_sum([1,2]))
# Prints 6

Real World Examples

Let’s look at some real world examples that will give you an idea of how decorators can be used.

Debugger

Let’s create a @debug decorator that will do the following, whenever the function is called.

  • Print the function’s name
  • Print the values of its arguments
  • Run the function with the arguments
  • Print the result
  • Return the modified function for use
from functools import wraps

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('Running function:', func.__name__)
        print('Positional arguments:', args)
        print('keyword arguments:', kwargs)
        result = func(*args, **kwargs)
        print('Result:', result)
        return result
    return wrapper

Let’s apply our @debug decorator to a function and see how this decorator actually works.

@debug
def hello(name):
    return "Hello " + name

hello("Bob")
# Prints Running function: hello
# Prints Positional arguments: ('Bob',)
# Prints keyword arguments: {}
# Prints Result: Hello Bob

You can also apply this decorator to any built-in function like this:

sum = debug(sum)
sum([1, 2, 3])
# Prints Running function: sum
# Prints Positional arguments: ([1, 2, 3],)
# Prints keyword arguments: {}
# Prints Result: 6

Timer

The following @timer decorator reports the execution time of a function. It will do the following:

  • Store the time just before the function execution (Start Time)
  • Run the function
  • Store the time just after the function execution (End Time)
  • Print the difference between two time intervals
  • Return the modified function for use
import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print("Finished in {:.3f} secs".format(end-start))
        return result
    return wrapper

Let’s apply this @timer decorator to a function.

@timer
def countdown(n):
    while n > 0:
        n -= 1

countdown(10000)
# Prints Finished in 0.005 secs
countdown(1000000)
# Prints Finished in 0.178 secs

 

Python Example for Beginners

Two Machine Learning Fields

There are two sides to machine learning:

  • Practical Machine Learning:This is about querying databases, cleaning data, writing scripts to transform data and gluing algorithm and libraries together and writing custom code to squeeze reliable answers from data to satisfy difficult and ill defined questions. It’s the mess of reality.
  • Theoretical Machine Learning: This is about math and abstraction and idealized scenarios and limits and beauty and informing what is possible. It is a whole lot neater and cleaner and removed from the mess of reality.

Data Science Resources: Data Science Recipes and Applied Machine Learning Recipes

Introduction to Applied Machine Learning & Data Science for Beginners, Business Analysts, Students, Researchers and Freelancers with Python & R Codes @ Western Australian Center for Applied Machine Learning & Data Science (WACAMLDS) !!!

Latest end-to-end Learn by Coding Recipes in Project-Based Learning:

Applied Statistics with R for Beginners and Business Professionals

Data Science and Machine Learning Projects in Python: Tabular Data Analytics

Data Science and Machine Learning Projects in R: Tabular Data Analytics

Python Machine Learning & Data Science Recipes: Learn by Coding

R Machine Learning & Data Science Recipes: Learn by Coding

Comparing Different Machine Learning Algorithms in Python for Classification (FREE)

Disclaimer: The information and code presented within this recipe/tutorial is only for educational and coaching purposes for beginners and developers. Anyone can practice and apply the recipe/tutorial presented here, but the reader is taking full responsibility for his/her actions. The author (content curator) of this recipe (code / program) has made every effort to ensure the accuracy of the information was correct at time of publication. The author (content curator) does not assume and hereby disclaims any liability to any party for any loss, damage, or disruption caused by errors or omissions, whether such errors or omissions result from accident, negligence, or any other cause. The information presented here could also be found in public knowledge domains.