Python Decorators

Reference

Primer on Python Decorators
Python Decorators 101


Introduction

What is decorator?

  • A function that takes another function
  • Extends the behavior of that function
  • Without explicitly modifying the function

Decorators

Simple Decorators

1
2
3
4
5
6
7
8
9
10
11
12
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")

return wrapper

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

say_whee = my_decorator(say_whee)

Input:

1
say_whee

Output:

1
<function __main__.my_decorator.<locals>.wrapper()>

Input:

1
say_whee()

Output:

1
2
3
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

1
2
3
4
5
6
7
8
9
10
11
12
from datetime import datetime

def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass # Hush, the neighbors are asleep
return wrapper

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

Input:

1
say_whee()

Output:

1
Whee!
1
say_whee = not_during_the_night(say_whee)

Input:

  • Time is 16:00 when I am running the following code.
1
say_whee()

Output:

1
Whee!

Syntactic Sugar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")

return wrapper

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

# say_whee = my_decorator(say_whee)

say_whee()
1
2
3
Something is happening before the function is called. 
Whee!
Something is happening after the function is called.

Decorating Functions with Arguments

1
2
3
4
5
6
7
8
9
10
11
def do_twice(func):
def wrapper_do_twice(one_arg):
func(one_arg)
func(one_arg)
return wrapper_do_twice

@do_twice
def greet(name):
print(f"Hello {name}")

greet('Zacks')
1
2
Hello Zacks
Hello Zacks
1
2
3
4
def greet_2(name):
print(f"Hello {name}")

do_twice(greet_2)('Zacks')
1
2
Hello Zacks
Hello Zacks
1
do_twice(greet)('Zacks')
1
2
3
4
Hello Zacks
Hello Zacks
Hello Zacks
Hello Zacks

1
2
3
4
5
6
7
8
9
10
11
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice

@do_twice
def greet(name):
print(f"Hello {name}")

greet('World')
1
2
Hello World
Hello World

Returning Values From Decorated Functions

Return values from decorated functions don’t get returned by default unless the decorator allows it. In this lesson, you’ll see how to get return values out of decorated functions by making a small change to the decorator.

1
2
3
4
5
6
7
8
9
10
11
12
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}'

return_greeting('World')
1
2
3
Creating greeting
Creating greeting
'Hi World'

1
return_greeting
1
<function __main__.do_twice.<locals>.wrapper_do_twice(*args, **kwargs)>

To make the function name more readable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

@do_twice
def return_greeting(name):
print("Creating greeting")
return f'Hi {name}'

return_greeting('World')
1
2
3
Creating greeting
Creating greeting
'Hi World'
1
return_greeting
1
<function __main__.return_greeting(name)>

Real-World Examples

Decorator Template

1
2
3
4
5
6
7
8
9
10
11
import functools

def decorator(func):
"""Decorator Template."""
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator

Timer example

What is the difference between !r and %r in Python?

In this lesson, you’ll see how to use decorators to measure the time a function takes to execute and print the duration to the console.

By using decorator @timer, we didn’t add or modify any code in function waste_some_time. And not only we get the returned value from waste_some_time, but also can we get the runtime for waste_some_time.

  • Timer example is copied from Decorator Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import functools
import time

def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
# Do something before
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
# Do something after
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time: .4f} secs") # wrapper version of the name
return value
return wrapper_timer
1
2
3
4
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
1
waste_some_time(1)
1
Finished 'waste_some_time' in 0.0039 secs
1
waste_some_time(999)
1
Finished 'waste_some_time' in  3.3956 secs

Debugging Code With Decorators

In this lesson, you’ll see how to create a decorator for debugging code. You’ll see how the @debug decorator prints out the arguments a function is called with as well as its return value every time the function is called.

  • Debug example is copied from Decorator Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import functools

def debug(func):
""""Print the function signature and return value."""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
# Do something before
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k} = {v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
# Do something after
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
1
2
3
4
5
6
@debug
def make_greeting(name, age=None):
if age is None:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
1
make_greeting('Zacks')
1
2
3
Calling make_greeting('Zacks')
'make_greeting' returned 'Howdy Zacks!'
'Howdy Zacks!'
1
make_greeting('Zacks', 25)
1
2
3
Calling make_greeting('Zacks', 25)
'make_greeting' returned 'Whoa Zacks! 25 already, you are growing up!'
'Whoa Zacks! 25 already, you are growing up!'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import functools
import math

def debug(func):
""""Print the function signature and return value."""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
# Do something before
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k} = {v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
# Do something after
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
1
2
3
4
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
return sum(1 / math.factorial(n) for n in range(terms))
1
2
3
4
5
6
7
8
9
10
11
12
approximate_e(5)
Calling factorial(0)
'factorial' returned 1
Calling factorial(1)
'factorial' returned 1
Calling factorial(2)
'factorial' returned 2
Calling factorial(3)
'factorial' returned 6
Calling factorial(4)
'factorial' returned 24
2.708333333333333

Slowing Down Code With Decorators

Decorators can be used to slow code down. Slowing down code can be useful when working with web services or when you want to rate limit a function. In this lesson, you’ll see how to create a @slow_down decorator that sleeps one second before it calls the decorated function.

It’s useful when you are visiting resources such as API. with some limitations.

  • Slowing Down example is copied from Decorator Template
1
2
3
4
5
6
7
8
9
10
11
12
13
import functools
import time

def slow_down(func):
"""Sleep 1 second before calling the function"""
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
# Do something before
time.sleep(1)
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_slow_down
1
2
3
4
5
6
7
@slow_down
def countdown(from_number):
if from_number < 1:
print('Liftoff!')
else:
print(from_number)
countdown(from_number - 1)
1
countdown(3)
1
2
3
4
3
2
1
Liftoff!

Registering Plugins With Decorators

The main benefit of registering Plugins with decorators is that you don’t have maintain a list of the functions you have created. You can easily know which plugins are exist.

1
2
3
4
5
6
7
8
import random

PLUGINS = dict()

def register(func):
"""Register a function as a plug-in"""
PLUGINS[func.__name__] = func
return func
1
2
3
4
5
6
7
8
9
@register
def say_hello(name):
return f"Hello {name}"

@register
def be_awsome(name):
return f"Yo {name}, together we are the awesomest!"

PLUGINS
1
2
{'say_hello': <function __main__.say_hello(name)>,
'be_awsome': <function __main__.be_awsome(name)>}
1
2
3
4
5
6
def randomly_greet(name):
greeter, greeter_func = random.choice(list(PLUGINS.items()))
print(f"Using {greeter!r}")
return greeter_func(name)

randomly_greet('Alice')
1
2
Using 'say_hello'
'Hello Alice'