Functions and Methods

Decorators

Basic Decorator

Let’s define a basic decorator.

import functools

class AuthorizationError(Exception):
    pass

def is_authenticated(f):
    """Require that user is authenticated"""
    @functools.wraps(f)
    def _authenticated(request, *args, **kwargs):
        if 'user' in request:
            return f(request, *args, **kwargs)
        else:
            raise AuthorizationError('You need to be logged in')
    return _authenticated

And decorate a function:

@is_authenticated
def get_balance(request):
    return 12345

Calling the function without being authenticated results in error:

>>> request = {'year': 2021}
>>> get_balance(request)
Traceback (most recent call last):
...
AuthorizationError: You need to be logged in

If there is a user authenticated, function returns result:

>>> request = { 'user': 'John', 'year': 2021}
>>> get_balance(request)
12345

Parameterized Decorator

import functools
import inspect

def not_user(username):
    def not_user_decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            func_args = inspect.getcallargs(f, *args, **kwargs)
            if func_args.get('username') == username:
                raise AuthorizationError('User is not authorized')
            else:
                return f(*args, **kwargs)
        return wrapper
    return not_user_decorator
@not_user("admin")
def get_food(username, food):
    return food

You can think of this scenario as calling a factory function which creates a decorator which is than applied to the function.

def get_food(username, food):
    return food

get_food = not_user("admin")(get_food)

Now the get_food function gives food if the user is not ‘admin’.

>>> get_food(username="john", food="apple")
'apple'

And raises an error in case of user is ‘admin’. Thanks to the inspect.getcallargs() function.

>>> get_food("admin", "orange")
Traceback (most recent call last):
...
AuthorizationError: User is not authorized

Supports also positional arguments:

>>> get_food(username="admin", food="orange")
Traceback (most recent call last):
...
AuthorizationError: User is not authorized

Decorator with optional arguments

def repeat(_func=None, *, num_times=2):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat

    if _func is None:
        return decorator_repeat
    else:
        return decorator_repeat(_func)

This solution uses the keyword-only arguments (PEP 3102). If positional argument is passed, it should be the decorated function. All decorator arguments are passed as keyword arguments.

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

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")
>>> say_whee()
Whee!
Whee!

>>> greet('John')
Hello John
Hello John
Hello John

Similar solution using partial function:

from functools import partial

def repeat(_func=None, *, num_times=2):
    @functools.wraps(_func)
    def wrapper_repeat(*args, **kwargs):
        for _ in range(num_times):
            value = _func(*args, **kwargs)
        return value

    if _func is None:
        return partial(repeat, num_times=num_times)
    else:
        return wrapper_repeat

Applying Multiple Decorators

You can think of decorators as being applied to what follows:

@not_user("admin")
@not_user("abcd")
@is_authenticated
def get_food(username, food):
    return food

@not_user("admin") is being applied to the result from the @not_user("abcd") decorator which in turn is applied to the result from the @is_authenticated decorator which is applied to the get_food() function.

Thus you can also remember that decorators are applied from bottom to top. First is applied the decorator at the bottom, next the decorator before it etc. until the top decorator.

Further Reading

Overloaded functions (multimethods)

According to Guido van Rossum, a function that has multiple versions, distinguished by the type of the arguments (Five-minute Multimethods in Python).

Multimethod implementation is implemented and available through the pygems.core.functools.multimethod() decorator.

Each time a function is decorated with the pygems.core.functools.multimethod() decorator, the decorator registers a new version of the function for the given sequence of argument types.

@multimethod(int, int)
def distance(a, b):
    return b - a

@multimethod(str, str)
def distance(a, b):
    return levenstein(a, b)