Function decorators allow us  to modify the behavior of a function or method  without having to change the source code of the function being decorated. 

They are used to wrap a function, modify its behavior, and return the modified function or a new function with the same or different name.

Decorators are quite common in Python and programming in general, Python offers an easier and more convenient syntax to  work with them. The syntax as shown below:

@decorator_function
def function_to_decorate():
      <block> 
copy

A decorator function is a higher order function that takes a function as an argument and  returns a new function commonly referred to as a wrapper . The wrapper function is used to modify or extend the behavior of the original function, it is also responsible of invoking the original function.

ExampleEdit & Run
def my_decorator(func):
    def wrapper(): 
        print("before calling the function.")  
        func()
        print("after calling the function.")
    return wrapper

@my_decorator
def my_function():
    print("Hello, World!")

my_function()
copy
Output:
before calling the function. Hello, World! after calling the function. [Finished in 0.010381205007433891s]

Without using the short hand syntax, we would need to explicitly pass the function to decorate as an argument to the decorator function, as shown below:

ExampleEdit & Run
def my_decorator(func):
    def wrapper(): 
        print("before calling the function.")  
        func()
        print("after calling the function.")
    wrapper()

def my_function():
    print("Hello, World!")

my_decorator(my_function)
copy
Output:
before calling the function. Hello, World! after calling the function. [Finished in 0.010171509347856045s]

When you call a decorated function with arguments, the arguments are first passed to the wrapper function, which then forwards them to the original function.

ExampleEdit & Run
def addition_decorator(func):
    def wrapper(a, b):
        return "%s + %s = %s"%(a, b, str(func(a, b)))
    return wrapper

@addition_decorator
def add(a, b):
    return a + b

print(add(60, 50))
copy
Output:
60 + 50 = 110 [Finished in 0.010186867788434029s]

If the wrapper function does not define any parameters and arguments are given to the decorated function, an error will be raised .

ExampleEdit & Run
def addition_decorator(func):
    def wrapper():
        return func()
    return wrapper

@addition_decorator
def add(a, b):
    return a + b

print(add(60, 50))
copy
Output:
Traceback (most recent call last):   File "<string>", line 10, in <module> TypeError: addition_decorator.<locals>.wrapper() takes 0 positional arguments but 2 were given [Finished in 0.010136979632079601s]

It is common to define wrapper functions which take arbitrary arguments as shown below:

ExampleEdit & Run
def addition_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@addition_decorator
def add(a, b):
    return a + b

print(add(60, 50))
copy
Output:
110 [Finished in 0.010232518427073956s]

A timing decorator function:

ExampleEdit & Run
from datetime import datetime
def timer(func):
    def wrapper(*args, **kwargs):
        start = datetime.now()
        print("Execution started at {}".format(start.strftime('%H:%M:%S.%f')))
        func(*args,**kwargs)
        stop = datetime.now()
        print("Execution stopped at", stop.strftime('%H:%M:%S.%f'))
        print("Function '{}' took {}".format(func.__name__, stop - start ))
    return wrapper

@timer
def add(a, b):
    print("%s + %s = %s"%(a, b, a + b))

add(9, 10)

@timer
def delayed_add(a, b):
    import time
    time.sleep(2)
    print(a + b)
    time.sleep(2)

delayed_add(20, 40)
copy
Output:
Execution started at 23:44:33.487805 9 + 10 = 19 Execution stopped at 23:44:33.487827 Function 'add' took 0:00:00.000022 Execution started at 23:44:33.487837 60 Execution stopped at 23:44:37.488024 Function 'delayed_add' took 0:00:04.000187 [Finished in 4.012987085618079s]

Decorator functions with arguments:

Decorator functions can also take additional arguments besides the function being decorated. The syntax of calling such a decorator is as shown below;

@my_decorator(arguments)
def my_function():
    <block>
copy

Defining a decorator function that takes additional arguments requires a slightly different syntax. Instead of defining the decorator function to take a single function as an argument, we define it to take any number of arguments, including the function being decorated. Then we define an inner function that takes the function as its argument and returns a wrapper function that uses the additional arguments passed to the decorator. Example:

ExampleEdit & Run
def repeat(times):
    def decorator(func):
         def wrapper(*args, **kwargs):
             for i in range(times):
                 func()
         return wrapper
    return decorator

@repeat(5)
def func():
    print("Hello, World!")

func()
copy
Output:
Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! [Finished in 0.011161096394062042s]

Multiple decorator functions

When multiple decorations are applied to a Python function, they are evaluated and applied in the order in which they appear. The first decoration will wrap the original function, and each successive decoration will wrap the result of the previous decoration. This means that the innermost decorator will be evaluated and applied first, followed by the decorators that wrap it from the outside in. Example:

ExampleEdit & Run
def decorator1(func):
    def wrapper():
        print("Decorator 1 before function call")
        func()
        print("Decorator 1 after function call")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2 before function call")
        func()
        print("Decorator 2 after function call")
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Hello, World!")

my_function()
copy
Output:
Decorator 1 before function call Decorator 2 before function call Hello, World! Decorator 2 after function call Decorator 1 after function call [Finished in 0.010843710042536259s]

When to use decorator Functions

Decorators can be used to add extra functionality to an existing function, such functionality can include: 

  1. Logging
  2. Caching
  3. Rate limiting
  4.  Authentication/Authorization
  5. Input/Output Sanitization
  6. Executing functions in parallel.
  7. Adding additional features to functions like argument binding, error handling and retry logic.
  8. Adding context managers to functions.