Decorator: Basics
TL;DR
- Decorators define reusable building blocks you can apply to a callable to modify its behavior without permanently modifying the callable itself.
- The @ syntax is just a shorthand (syntax sugar) for calling the decorator on an input function.
- Multiple decorators on a single function are applied bottom to top (decorator stacking).
- Use the
functools.wraps
helper in every decorator to carry over metadata from the undecorated callable to the decorated one. - Decorators are not a cure-all and they should not be overused
What is decorator?
Decorators provide a simple syntax for calling higher-order functions.
By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.
In other words, decorators “decorate” or “wrap” another function and let you execute code before and after the wrapped function runs.
- Allow you to define reusable building blocks that can change or extend the behavior of other functions
- Let you do that without permanently modifying the wrapped function itself. The function’s behavior changes only when it’s decorated.
Under the hood, a decorator is a callable that takes a callable as input and returns another callable.
Functions
Before you can understand decorators, you must first understand how functions work.
In Python, functions
- return a value based on the given arguments
- are first-class objects. I.e., functions can be passed around and used as arguments.
Inner Functions
Inner functions are functions defined inside other functions.
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()
The order in which the inner functions are defined does not matter.
The inner functions are not defined until the parent function is called. They are locally scoped to
parent()
, i.e., they only exist inside theparent()
function as local variables.
Returning Functions From Functions
Python also allows you to use functions as return values. For example:
def parent(num):
def first_child():
return "Hi, I am the first child."
def second_child():
return "Hi, I am the second child."
# Return function name without the parentheses means returning a reference to the function
# In contrast, function name with parentheses refers to the result of evaluating the functions.
return first_child if num == 1 else second_child
Simple Decorators
Put simply: decorators wrap a function, modifying its behavior.
Example:
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
def say_hi():
print("Hi")
say_hi = my_decorator(say_hi)
>>> say_hi()
Before function
Hi
After function
Syntactic Sugar
The way we decorated say_hi()
above is a little clunky.
- We type the function name
say_hi()
three times - The decoration gets a bit hidden away below the definition of the function.
To facilitate, Python allows you to use decorators in a simpler way with the @
symbol (sometimes called the “pie” syntax).
The followings are equivalent:
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
def say_hi():
print("Hi")
say_hi = my_decorator(say_hi)
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
@my_decorator # an easier way of say_hi = my_decorator(say_hi)
def say_hi():
print("Hi")
>>> say_hi()
Before function
Hi
After function
Naming of the Inner Function
You can name your inner function whatever you want, and a generic name like wrapper()
is usually okay. You can also name the inner function with the same name as the decorator but with a wrapper_
prefix.
Example:
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice
Decorating Functions With Arguments
To allow the decorator accept an arbitrary number of positional and keyword arguments, use *args
and **kwargs
in the inner wrapper function.
It use the
*
and**
operators in thewrapper
closure definition to collect all positional and keyword arguments and stores them in variables (args
andkwargs
).The
wrapper
closure then forwards the collected arguments to the original input function using the * and ** “argument unpacking” operators.
Example:
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def say_hi():
print("Hi")
@do_twice
def greet(name):
print(f"Hello {name}")
>>> say_hi()
Hi
Hi
>>> greet("World")
Hello World
Hello World
Returning Values From Decorated Functions
Make sure the wrapper function returns the return value of the decorated function.
Example
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def return_greeting(name):
print("Creating greeting")
return f"Hi {name}"
>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
Hi Adam
Another example
def uppercase(func):
def wrapper():
original_result = func()
modified_result = original_result.upper()
return modified_result
return wrapper
@uppercase
def greet():
return "Hello!"
Getting the Indentity Information
What a decorator do is replacing one function with another. One downside of this process is that it “hides” some of the metadata attached to the original (undecorated) function.
For example, the original function name, its docstring, and parameter list are hidden by the wrapper closure:
def greet():
"""Return a friendly greeting."""
return 'Hello!'
decorated_greet = uppercase(greet)
If you try to access any of that function metadata, you’ll see the wrap- per closure’s metadata instead:
>>> greet.__name__
'greet'
>>> greet.__doc__
'Return a friendly greeting.'
>>> decorated_greet.__name__
'wrapper'
>>> decorated_greet.__doc__
None
This makes debugging and working with the Python interpreter awkward and challenging 🤪.
A quick fix for this is to use the @functools.wraps
decorator in Python’s standard library, which will preserve information about the original function.
Technical Detail
The @functools.wraps
decorator uses the function functools.update_wrapper()
to update special attributes like __name__
and __doc__
that are used in the introspection.
Example
import functools
def do_twice(func):
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
>>> say_hi
<function __main__.say_hi>
>>> say_hi.__name__
say_hi
>>> help(say_hi)
Help on function say_hi in module __main__:
say_hi()
As a best practice, you should use functools.wraps
in all of the decorators you write yourself. It doesn’t take much time and it will save you (and others) debugging headaches down the road.
Real World Examples
A good boilerplate template for building more complex decorators:
import functools
def decorator(func):
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator
Timing Functions
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
runtime = end_time - start_time
print(f"Finished {func.__name__!r}: in {runtime:.4f} secs.")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i * 2 for i in range(100)])
>>> waste_some_time(1000)
Finished 'waste_some_time': in 0.0130 secs.
Debugging Code
import functools
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(arg) for arg in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}")
return value
return wrapper_debug
@debug
def make_greeting(name, age=None):
if not age:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
>>> make_greeting("Richard", age=20)
Calling make_greeting('Richard', age=20)
'make_greeting' returned 'Whoa Richard! 20 already, you are growing up!'
Whoa Richard! 20 already, you are growing up!
Registering Plugins
Decorators don’t have to wrap the function they’re decorating. They can also simply register that a function exists and return it unwrapped.
E.g., to create a light-weight plug-in architecture:
import random
PLUGINS = dict()
def register(func):
"""Register a function as a plug-in"""
# Simply stores a reference to the decorated function in the global PLUGINS dict
# Note that you do not have to write an inner function or use @functools.wraps in this example,
# as you are returning the original function unmodified.
PLUGINS[func.__name__] = func
return func
@register
def say_hello(name):
print(f"Hello {name}")
@register
def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"
def randomly_greet(name):
"""Randomly chooses one of the registered functions to use."""
greeter, greeter_func = random.choice(list(PLUGINS.items()))
print(f"Using {greeter!r}")
return greeter_func(name)
The `@register` decorator simply stores a reference to the decorated function in the global `PLUGINS` dict.
Because
@register
def be_awesome(name):
is equivalent to
be_awesome = register(be_awesome)
Therefore, whenever adding @register
on top of the function, this function is registered in PLUGINS
. In other words, the PLUGINS
dictionary contains references to thefunction object.
Note that you do not have to write an inner function or use @functools.wraps
in this example because you are returning the original function unmodified.
>>> PLUGINS
{'be_awesome': <function __main__.be_awesome>,
'say_hello': <function __main__.say_hello>}
>>> randomly_greet("Alice")
Using 'say_hello'
Hello Alice
👍 Main benefit: You do not need to maintain a list of which plugins exist. That list is created when the plugins register themselves. This makes it trivial to add a new plugin: just define the function and decorate it with @register
.