Python Decorators
Decorators allow you to attach new functionality to a method without modifying it’s behavior. This can be easily implemented in python because functions are treated as first class citizens (i.e a can be passed as arguments or returned from functions).
To put in simple terms:
A Decorator is a function the takes a function and returns a function.
They are used extensively in some popular frameworks like Flask.
Dummy Decorator#
This is a simple decorator that prints a statement before and after a function is called.
Declare#
def dec(f: Callable) -> Callable:
def g(*args, **kwargs):
print('Function starts')
r = f(*args, **kwargs)
print(f) # print function name and location in memory
print('Function ends')
return r # return result
return g # returns a new "decorated" function
-
Line 1: We define a new function (which will soon be our decorator) called dec which takes a function f as a parameter. I have added few type hints using
import typing
but it’s not necessary and not enforced by the interpreter. -
Line 2: We define a new inner function g which takes all the ordinary arguments and keyword arguments that function f takes.
-
Line 3: Just an example of something that happens before executing function f.
-
Line 4: Call function f with all of it’s arguments and store result in variable r.
-
Line 5: Just and example of something that happens after executing function f.
-
Line 6: Returns the result of function f in order to keep it working as intended.
-
Line 7: Returns the newly decorated function g.
Usage#
Now let’s decorate a simple add
function with the new decorator:
@dec
def add(x: float, y: float):
return x + y
add(5, 6)
print(add)
Output#
Function starts
<function add at 0x7f6735d528b0>
Function ends
11
<function dec.<locals>.g at 0x7f6735d52940>
The difference in function name and memory address (line 2 & line 5) will be discussed below.
Timer Decorator#
Here is an applicable example of a decorator that enables you to easily determine how long does a function takes in order to finish execution.
Declare#
def timer(f: Callable) -> Callable:
def g(*args, **kwargs):
t_start = time.monotonic()
r = f(*args, **kwargs)
t_end = time.monotonic()
print(f"It took {t_end - t_start} to execute \"{f.__name__}\" function")
return r
return g
Usage#
You replace the @dec
with @timer
or even stack multiple decorators on top of each other:
@dec
@timer
def add(x: float, y: float):
return x + y
add(5, 6)
print(add)
Output#
Function starts
It took 5.5779964895918965e-06 to execute "add" function
<function timer.<locals>.g at 0x7f1da16d2940>
Function ends
11
- Line 1: Output of decorator
@dec
. - Line 2: Output of decorator
@timer
. - Line 3, 4: The rest of
@dec
’s output. - Line 5: Output of function
add
.
Tracker Decorator#
Let’s assume you want to keep track of the output of different functions in some sort of a global set. This can be useful for some sort of a simple unit testing solution.
Declare#
TEST = set()
def track(f: Callable) -> Callable:
def g(*args, **kwargs):
r = f(*args, **kwargs)
t = (f.__name__, args, kwargs, r)
TEST.add(t)
return r
return g
Not much different than the previous decorators, only is adding a tuple that contains the function name, arguments and result to a global set called TEST
.
Usage#
print(add(5, 6))
print(add(3, 7))
print(add(11, 17))
print(add)
print(STATE)
assert STATE == {('add', (11, 17), 28), ('add', (5, 6), 11), ('add', (3, 7), 10)}
print('All tests passed successfully')
If anything went wrong with the assert statement; the interpreter will halt execution and raise and assert exception. AssertionError
Output#
11
10
28
<function track.<locals>.g at 0x7f01e1b399d0>
[('add', (5, 6), {}, 11), ('add', (3, 7), {}, 10), ('add', (11, 17), {}, 28)]
All tests passed successfully
functools.wraps#
In order to add additional functionality to a method, the decorator generate a new function with a new name in a new memory address which might confuse some tools and debuggers.
To avoid renaming your function and keep it’s original docstring, use wraps
from functools
:
from functools import wraps
def dec(f: Callable) -> Callable:
@wraps(f)
def g(*args, **kwargs):
print('Function starts')
r = f(*args, **kwargs)
print(f) # print function name and location in memory
print('Function ends')
return r # return result
return g