Python Decorators#
Python decorators are a powerful feature of the Python programming language that allow computer scientists to modify or enhance the behavior of functions without changing their source code. Essentially, a decorator is a function that takes another function as input and returns a new function as output.
Nested functions & Inner Functions#
In Python, it is possible to define functions inside other functions. Such functions are called inner functions.
def parent():
print("Printing from the parent() function")
def first_child():
print("Printing from the first_child() function")
def second_child():
print("Printing from the second_child() function")
second_child()
first_child()
What will the call to parent
return?
parent()
Show code cell output
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function
Tip
inner functions are local variables accessible inside the function definition (scope dependent).
Functions are variables too#
We can pass a function as an argument to another function in Python. For Example
def say_hello(name):
return f"Hello {name:s}"
def be_awesome(name):
return f"Yo {name:s}, together we are the awesomest!"
def greet_bob(greeter_func):
return greeter_func("Bob")
note
greet_bob
takes a function as argument. In Python, functions can be passed around and used as arguments
print(greet_bob(say_hello))
print(greet_bob(be_awesome))
Show code cell output
Hello Bob
Yo Bob, together we are the awesomest!
Since inner functions are variables, python functions can return functions like any other variable.
def parent():
def inner():
return "Hi, I am the inner function"
return inner
parent()
<function __main__.parent.<locals>.inner()>
The returned value is a function defined in the parent
scope as inner
parent()()
'Hi, I am the inner function'
A first useless decorator#
a decorator wraps a function into a function
def deco(func):
def wrapper(*args, **kwargs):
print(f"Something before calling the function.")
result = func(*args, **kwargs)
print("Something after calling the function.")
return result
return wrapper
def say_whee():
print("Whee!")
say_whee = deco(say_whee)
Can you guess what happens when you call say_whee()
?
say_whee()
Show code cell output
Something before calling the function.
Whee!
Something after calling the function.
What happened?
>>> say_whee = deco(say_whee) # decoration of `say_whee`
>>> say_whee
<function deco.<locals>.wrapper at 0x7f3c5dfd42f0>
Syntactic Sugar! @
#
@deco
def say_whee():
""" Useful Documentation here!"""
print("Whee!")
say_whee()
Something before calling the function.
Whee!
Something after calling the function.
What is decorated?#
help(say_whee)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
How do we fix this? e.g., propagating documentation –> @functools.wraps
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Something before
result = func(*args, **kwargs)
# Something after
return result
return wrapper
@deco
def say_whee():
""" Useful Documentation here!"""
print("Whee!")
say_whee()
help(say_whee)
Whee!
Help on function say_whee in module __main__:
say_whee()
Useful Documentation here!
Some useful examples of decorators#
debug
- provides verbose information when activatedtimeit
- gives runtime of a functionmemoize
- A quick cachingfunctools.lru_cache
- a LRU caching mechanism (built-in)astropy.units.quantity_input
to validate the units of arguments/outputs
debug
#
Imagine you have a long complex code. Instead of having to introduce various prints, or intentional errors, you can set a decorator to report or even break the code at a given postion and on demand.
VERBOSE = True
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
if VERBOSE:
print("{0}(\n\t{1}, \n\t{2}\n) -> {3}".format(
func.__name__, args, kwargs, value ))
return value
return wrapper
@debug
def do_something(a, b, c=3):
return a * b, 2 * c
do_something(1, 2, 3)
do_something(
(1, 2, 3),
{}
) -> (2, 6)
(2, 6)
In the above example, you can set VERBOSE
to show or not information. This could help also create a breakpoint for easier debugging.
timeit
#
from functools import wraps
import time
def timeit(func):
"""Prints the runtime of decorated functions"""
@wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
@timeit
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
waste_some_time(999)
Finished 'waste_some_time' in 3.5788 secs