Sometimes you want to modify an existing function without changing its source code. A common example is adding extra processing (e.g. logging, timing, etc.) to the function.
That’s where decorators come in.
A decorator is a function that accepts a function as input and returns a new function as output, allowing you to extend the behavior of the function without explicitly modifying it.
Every decorator is a form of metaprogramming.
Metaprogramming is about creating functions and classes whose main goal is to manipulate code (e.g., modifying, generating, or wrapping existing code).
Python Functions
Before you can understand decorators, you must first know how functions work.
Pass Function as Arguments
In Python, functions are first-class objects. This means that they can be passed as arguments, just like any other object (string, int, float, list etc.).
If you have used functions like map or filter before, then you already know about it.
Consider the following example.
def hello1():
print("Hello World")
def hello2():
print("Hello Universe")
def greet(func):
func()
greet(hello1)
# Prints Hello World
greet(hello2)
# Prints Hello Universe
Here, hello1()
and hello2()
are two regular functions and are passed to the greet()
function.
Note that these functions are passed without parentheses. This means that you are just passing their reference to greet()
function.
Inner Functions
Functions can be defined inside other functions. Such functions are called Inner functions. Here’s an example of a function with an inner function.
def outer_func():
def inner_func():
print("Running inner")
inner_func()
outer_func()
# Prints Running inner
Returning a Function
Python also allows you to return a function. Here is an example.
def greet():
def hello(name):
print("Hello", name)
return hello
greet_user = greet()
greet_user("Bob")
# Prints Hello Bob
Again, the hello()
function is returned without parentheses. This means that you are just returning its reference.
And this reference is assigned to greet_user
, due to which you can call greet_user
as if it were a regular function.
Simple Decorator
Now that you’ve learned that functions are just like any other object in Python, you are now ready to learn decorators.
Let’s see how decorators can be created in Python. Here is a simple example decorator that does not modify the function it decorates, but rather prints a message before and after the function call.
def decorate_it(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
def hello():
print("Hello world")
hello = decorate_it(hello)
hello()
# Prints Before function call
# Prints Hello world
# Prints After function call
Whatever function you pass to decorate_it()
, you get a new function that includes the extra statements that decorate_it()
adds. A decorator doesn’t actually have to run any code from func
, but decorate_it()
calls func
part way through so that you get the results of func
as well as all the extras.
Simply put, a decorator is a function that takes a function as input, modifies its behavior and returns it.
The so-called decoration happens when you call the decorator and pass the name of the function as an argument.
hello = decorate_it(hello)
Here you applied the decorator manually.
Syntactic Sugar
As an alternative to the manual decorator assignment above, just add @decorator_name before the function that you want to decorate.
The following example does the exact same thing as the first decorator example:
def decorate_it(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@decorate_it
def hello():
print("Hello world")
hello()
# Prints Before function call
# Prints Hello world
# Prints After function call
So, @decorate_it is just an easier way of saying hello = decorate_it(hello).
Decorating Functions that Takes Arguments
Let’s say you have a function hello()
that accepts an argument and you want to decorate it.
def decorate_it(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@decorate_it
def hello(name):
print("Hello", name)
hello("Bob")
# Prints _wrapper() takes 0 positional arguments but 1 was given
Unfortunately, running this code raises an error. Because, the inner function wrapper()
does not take any arguments, but we passed one argument.
The solution is to include *args and **kwargs in the inner wrapper function. The use of *args and **kwargs is there to make sure that any number of input arguments can be accepted.
Let’s rewrite the above example.
def decorate_it(func):
def wrapper(*args, **kwargs):
print("Before function call")
func(*args, **kwargs)
print("After function call")
return wrapper
@decorate_it
def hello(name):
print("Hello", name)
hello("Bob")
# Prints Before function call
# Prints Hello Bob
# Prints After function call
Returning Values from Decorated Functions
What if the function you are decorating returns a value? Let’s try that quickly:
def decorate_it(func):
def wrapper(*args, **kwargs):
print("Before function call")
func(*args, **kwargs)
print("After function call")
return wrapper
@decorate_it
def hello(name):
return "Hello " + name
result = hello("Bob")
print(result)
# Prints Before function call
# Prints After function call
# Prints None
Because the decorate_it()
doesn’t explicitly return a value, the call hello("Bob")
ended up returning None.
To fix this, you need to make sure the wrapper function returns the return value of the inner function.
Let’s rewrite the above example.
def decorate_it(func):
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
@decorate_it
def hello(name):
return "Hello " + name
result = hello("Bob")
print(result)
# Prints Before function call
# Prints After function call
# Prints Hello Bob
Preserving Function Metadata
Copying decorator metadata is an important part of writing decorators.
When you apply a decorator to a function, important metadata such as the name, doc string, annotations, and calling signature are lost.
For example, the metadata in our example would look like this:
def decorate_it(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@decorate_it
def hello():
'''function that greets'''
print("Hello world")
print(hello.__name__)
# Prints wrapper
print(hello.__doc__)
# Prints None
print(hello)
# Prints <function decorate_it.<locals>.wrapper at 0x02E15078>
To fix this, apply the @wraps decorator from the functools library to the underlying wrapper function.
from functools import wraps
def decorate_it(func):
@wraps(func)
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@decorate_it
def hello():
'''function that greets'''
print("Hello world")
print(hello.__name__)
# Prints hello
print(hello.__doc__)
# Prints function that greets
print(hello)
# Prints <function hello at 0x02DC5BB8>
Whenever you define a decorator, do not forget to use @wraps, otherwise the decorated function will lose all sorts of useful information.
Unwrapping a Decorator
Even if you’ve applied a decorator to a function, you sometimes need to gain access to the original unwrapped function, especially for debugging or introspection.
Assuming that the decorator has been implemented using @wraps, you can usually gain access to the original function by accessing the __wrapped__
attribute.
For example,
from functools import wraps
def decorate_it(func):
@wraps(func)
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@decorate_it
def hello():
print("Hello world")
original_hello = hello.__wrapped__
original_hello()
# Prints Hello world
Nesting Decorators
You can have more than one decorator for a function. To demonstrate this let’s write two decorators:
- double_it() that doubles the result
- square_it() that squares the result
from functools import wraps
def double_it(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result * 2
return wrapper
def square_it(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result * result
return wrapper
You can apply several decorators to a function by stacking them on top of each other.
@double_it
@square_it
def add(a,b):
return a + b
print(add(2,3))
# Prints 50
Here, the addition of 2 and 3 was squared first then doubled. So you got the result 50.
result = ((2+3)^2)*2
= (5^2)*2
= 25*2
= 50
Execution order of decorators
The decorator closest to the function (just above the def) runs first and then the one above it.
Let’s try reversing the decorator order:
@square_it
@double_it
def add(a,b):
return a + b
print(add(2,3))
# Prints 100
Here, the addition of 2 and 3 was doubled first then squared. So you got the result 100.
result = ((2+3)*2)^2
= (5*2)^2
= 10^2
= 100
Applying Decorators to Built-in Functions
You can apply decorators not only to the custom functions but also to the built-in functions.
The following example applies the double_it()
decorator to the built-in sum() function.
from functools import wraps
def double_it(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result * 2
return wrapper
double_the_sum = double_it(sum)
print(double_the_sum([1,2]))
# Prints 6
Real World Examples
Let’s look at some real world examples that will give you an idea of how decorators can be used.
Debugger
Let’s create a @debug decorator that will do the following, whenever the function is called.
- Print the function’s name
- Print the values of its arguments
- Run the function with the arguments
- Print the result
- Return the modified function for use
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Running function:', func.__name__)
print('Positional arguments:', args)
print('keyword arguments:', kwargs)
result = func(*args, **kwargs)
print('Result:', result)
return result
return wrapper
Let’s apply our @debug decorator to a function and see how this decorator actually works.
@debug
def hello(name):
return "Hello " + name
hello("Bob")
# Prints Running function: hello
# Prints Positional arguments: ('Bob',)
# Prints keyword arguments: {}
# Prints Result: Hello Bob
You can also apply this decorator to any built-in function like this:
sum = debug(sum)
sum([1, 2, 3])
# Prints Running function: sum
# Prints Positional arguments: ([1, 2, 3],)
# Prints keyword arguments: {}
# Prints Result: 6
Timer
The following @timer decorator reports the execution time of a function. It will do the following:
- Store the time just before the function execution (Start Time)
- Run the function
- Store the time just after the function execution (End Time)
- Print the difference between two time intervals
- Return the modified function for use
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print("Finished in {:.3f} secs".format(end-start))
return result
return wrapper
Let’s apply this @timer decorator to a function.
@timer
def countdown(n):
while n > 0:
n -= 1
countdown(10000)
# Prints Finished in 0.005 secs
countdown(1000000)
# Prints Finished in 0.178 secs