Decorators

Simple Decorators

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

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

Put simply: decorators wrap a function, modifying its behavior.

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()
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
12
13
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")
Hello World
Hello World

Returning Values From Decorated Functions

make sure the wrapper function returns the return value of the decorated function

1
2
3
4
5
6
7
8
9
10
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}"
1
2
3
4
>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

A Few Real World Examples

1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import functools
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() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer

def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
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)
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug

Fancy Decorators

Decorating Classes

Some commonly used decorators that are even built-ins in Python are @classmethod, @staticmethod, and @property. The @classmethod and @staticmethod decorators are used to define methods inside a class namespace that are not connected to a particular instance of that class. The @property decorator is used to customize getters and setters for class attributes. Expand the box below for an example using these decorators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from decorators import timer

@timer
class TimeWaster:
def __init__(self, max_num):
self.max_num = max_num

def waste_time(self, num_times):
for _ in range(num_times):
sum([i**2 for i in range(self.max_num)])
>>> tw = TimeWaster(1000)
Finished 'TimeWaster' in 0.0000 secs

>>> tw.waste_time(999)

Here, @timer only measures the time it takes to instantiate the class

Nesting Decorators

1
2
3
4
5
6
from decorators import debug, do_twice

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

Think about this as the decorators being executed in the order they are listed. In other words, @debug calls @do_twice, which calls greet(), or debug(do_twice(greet())):

1
2
3
4
5
>>> greet("Eva")
Calling greet('Eva')
Hello Eva
Hello Eva
'greet' returned None

Decorators With Arguments

1
2
3
4
5
6
7
8
9
def repeat(num_times):
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
return decorator_repeat
1
2
3
4
5
6
7
8
9
@repeat(num_times=4)
def greet(name):
print(f"Hello {name}")

>>> greet("World")
Hello World
Hello World
Hello World
Hello World

you can also define decorators that can be used both with and without arguments. Most likely, you don’t need this, but it is nice to have the flexibility.

1
2
3
4
5
6
7
8
def name(_func=None, *, kw1=val1, kw2=val2, ...):  # 1
def decorator_name(func):
... # Create and return a wrapper function.

if _func is None:
return decorator_name # 2
else:
return decorator_name(_func) # 3
1
2
3
4
5
6
7
8
9
10
11
12
13
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)

These examples show that @repeat can now be used with or without arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@repeat
def say_whee():
print("Whee!")

@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")

>>> say_whee()
Whee!
Whee!

>>> greet("Penny")
Hello Penny
Hello Penny
Hello Penny

Stateful Decorators

function attributes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import functools

def count_calls(func):
@functools.wraps(func)
def wrapper_count_calls(*args, **kwargs):
wrapper_count_calls.num_calls += 1
print(f"Call {wrapper_count_calls.num_calls} of {func.__name__!r}")
return func(*args, **kwargs)
wrapper_count_calls.num_calls = 0
return wrapper_count_calls

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

>>> say_whee()
Call 1 of 'say_whee'
Whee!

>>> say_whee()
Call 2 of 'say_whee'
Whee!

>>> say_whee.num_calls
2

Classes as Decorators

1
2
3
4
5
6
7
class Counter:
def __init__(self, start=0):
self.count = start

def __call__(self):
self.count += 1
print(f"Current count is {self.count}")
1
2
3
4
5
6
7
8
9
>>> counter = Counter()
>>> counter()
Current count is 1

>>> counter()
Current count is 2

>>> counter.count
2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import functools

class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0

def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func(*args, **kwargs)

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

>>> say_whee()
Call 1 of 'say_whee'
Whee!

>>> say_whee()
Call 2 of 'say_whee'
Whee!

>>> say_whee.num_calls
2

Reference

https://realpython.com/primer-on-python-decorators/#syntactic-sugar


Decorators
http://chenxindaaa.com/Programming/Python/Python Cookbook/Decorators/
Author
chenxindaaa
Posted on
December 14, 2019
Licensed under