what is it?
a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it
simple example
some background knowledge - when using functions as first-class objects - a function name without parentheses is a reference to a function, while a function name with trailing parentheses calls the function and refers to its return value
def 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 = decorator(say_whee)
>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.using the pie syntax
def 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
@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.convention is to usually name the inner function wrapper()
handling arguments
use *args and **kwargs in the inner wrapper function to be able to decorate functions with varying amounts of arguments
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}")
@do_twice
def say_whee():
print("Whee!)
>>> greet("World")
Hello World
Hello World
>>> say_whee()
Whee!
Whee!returning values from decorated functions
have to return the decorated function in the wrapper function
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("Adam")
Creating greeting
Creating greeting
'Hi Adam'introspection
the ability of an object to know about its own attributes at runtime. after being decorated, functions report being the wrapper functions
to fix this, decorators should use the @functools.wraps decorator, which will preserve information about the original function
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 say_whee():
print("Whee!")
>>> say_whee
<function say_whee at 0x7ff79a60f2f0>
>>> say_whee.__name__
'say_whee'boilerplate
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_decoratornesting decorators
>>> @debug
... @do_twice
... def greet(name):
... print(f"Hello {name}")@debug calls @do_twice, which calls greet(), or debug(do_twice(greet()))
decorators with arguments
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_repeatto make the arguments optional, they have to be specified by keyword. the asterisk (*) syntax, specifies all the following parameters are keyword-only
def name(_func=None, *, key1=value1, key2=value2, ...):
def decorator_name(func):
... # Create and return a wrapper function.
if _func is None:
return decorator_name
else:
return decorator_name(_func)NB the function to decorate is only passed in directly if the decorator is called without arguments
the _func argument acts as a marker, noting whether the decorator has been called with arguments or not
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)
@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 Pennythis is how the above functions look without the @ decorator
def say_whee():
print("Whee!")
say_whee = repeat(say_whee)
def greet(name):
print(f"Hello {name}")
greet = repeat(num_times=3)(greet)classes as decorators
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__}()")
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__init__() method must store a reference to the function, and it can do any other necessary initialisation. use the functools.update_wrapper() function instead of @functools.wraps
for a class instance to be callable, you implement the special .__call__() method