Python Decorators

Definition: a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

Functions

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

It’s possible to define functions inside other functions. Such functions are called inner functions. For example:

def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

Note that the order in which the inner functions are defined does not matter. Like with any other functions, the printing only happens when the inner functions are executed. Furthermore, the inner functions are not defined until the parent function is called. They are locally scoped to parent(): they only exist inside the parent() function as local variables.

Python also allows you to use functions as return values, which returning a reference to the function.

Simple Decorators

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

The outpur of executing say_whee()is:

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

Put simply: decorators wrap a function, modifying its behavior.

Usingfunctools.wrapsto create a decorator that wraps functions in a matter that make them indistinguishable from the original function.

Syntactic Sugar

Python allows you to use decorators in a simpler way with the @ symbol, sometimes called the “pie” syntax. The following example does the exact same thing as the first decorator example:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_whee():
    print("Whee!")

Decorating Functions With Arguments

Use *args and **kwargs in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments.

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

Returning Values From Decorated Functions

What happens to the return value of decorated functions? Well, that’s up to the decorator to decide. You need to make sure the wrapper function returns the return value of the decorated function.

Decorating Classes

There are two different ways you can use decorators on classes. The first one is very close to what you have already done with functions: you can decorate the methods of a class. This was one of the motivations for introducing decorators back in the day.

Some commonly used decorators that are even built-ins in Python are @classmethod, @staticmethod, and @property. The @classmethod and @staticmethod decorators are used to define methods inside a class namespace that are not connected to a particular instance of that class. The @property decorator is used to customize getters and setters for class attributes. Expand the box below for an example using these decorators.

The other way to use decorators on classes is to decorate the whole class. This is, for example, done in the new dataclasses module in Python 3.7:

from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

A common use of class decorators is to be a simpler alternative to some use-cases of metaclasses. In both cases, you are changing the definition of a class dynamically.

Writing a class decorator is very similar to writing a function decorator. The only difference is that the decorator will receive a class and not a function as an argument. In fact, all the decorators you saw above will work as class decorators. When you are using them on a class instead of a function, their effect might not be what you want.

Decorating a class does not decorate its methods.

#!/usr/bin/env python3

def decorate(cls):
    print(cls)
    return cls

@decorate
class Foo: pass

This code will work both in python2 and python3:

$ python example.py
__main__.Foo
$ python3 example.py
<class '__main__.Foo'>

Nesting Decorators

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

from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")

Classes as Decorators

urthermore, the class needs to be callable so that it can stand in for the decorated function, so implementing the function __call__() to create a callable object.

class decorator2:
    
    def __init__(self, f):
        self.f = f
        
    def __call__(self):
        print("Decorating", self.f.__name__)
        self.f()

@decorator2
def foo():
    print("inside foo()")

foo()

Reference:

https://realpython.com/primer-on-python-decorators/#decorating-classes