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.

Example:

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

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

def greet(func):
    func()

greet(hello1)    # Hello World
greet(hello2)    # 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.

Example:

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

outer_func()     # Running inner

Returning a Function

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

Example:

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

greet_user = greet()

greet_user("Bob")    # 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.

Example:

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()

Output:

Before function call
Hello world
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:

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()

Output:

Before function call
Hello world
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.

Example:

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")

Output:

_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.

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")

Output:

Before function call
Hello Bob
After function call

Returning Values from Decorated Functions

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

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):
    return "Hello " + name

result = hello("Bob")

print(result)

Output:

Before function call
After function call
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.

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)

Output:

Before function call
After function call
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:

Example:

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__)
print(hello.__doc__)
print(hello)

Output:

wrapper
None
<function decorate_it.<locals>.wrapper at 0x02E15078>

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

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():
    '''function that greets'''
    print("Hello world")

print(hello.__name__)
print(hello.__doc__)
print(hello)

Output:

hello
function that greets
<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,

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()

Output:

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

Example:

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.

Example:

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

print(add(2,3))

Output:

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:

Example:

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

print(add(2,3))

Output:

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.

Example:

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]))

Output:

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

Example:

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.

Example:

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

hello("Bob")

Output:

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

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

Example:

sum = debug(sum)
sum([1, 2, 3])

Output:

Running function: sum
Positional arguments: ([1, 2, 3],)
keyword arguments: {}
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

Example:

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.

Example:

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

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