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
Primer on Python Decorators at Real Python
PEP 318 – Decorators for Functions and Methods
Decorators at PythonTips
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)