Function Decorators To Reduce Labor
syntactic sugar is nice 10 Jan 2021TL;DR
I wrote a set of Python decorators that I use while writing/debugging code. Here’s the link to the gist. Although if you have an incredibly complex piece of python code, odds are you should just use a IDE or debugger…
Read more if you want a dramatization with the reasoning.
Introduction & Why
I end up using python as a prototyping language because it has dynamic typing and deep library support. In addition, Python is a popular language for statistical libraries, so I also end up working with numpy arrays frequently.
A part of the writing process is annoying because I need to frequently debug complicated processes (and I’m not in a situation to use a python debugger or IDE). Often times I fall into a process like this…
data = ... # some data matrix
print(data.shape)
result = SOME_OPERATION(data)
print(result.shape) # Something is wrong
# Change the code
data = ... # some data matrix
print(data.shape)
result = SOME_OPERATION_MODIFIED(data)
print(result.shape) # Something is still wrong
# repeat
or maybe something like this…
very_long_training_process()
print("done training")
send_me_an_email('ted@email.com')
or maybe something like this…
out1 = function_that_does_something(1,2,3)
out2 = function_that_does_something(0,0,0)
out3 = function_that_does_something(-1,-2,-3)
out4 = function_that_does_something('a','b','c')
print(out1, out2, out3, out4)
At some point I got tired of repeating these patterns, and knew there must be some easy way of doing these things.
An example of a “solution”
The solution I was thinking of was something like this:
def print_output(func, *args, **kwargs):
a = func(*args, **kwargs)
print(a)
return a
except that it’s so clunky to use. Every time I want to debug something, I need to write a handful more code to get what I want. For example:
# function_that_does_something will be abbreviated to ftds
out1 = print_output(ftds, 1, 2, 3)
out2 = print_output(ftds, 0, 0, 0)
out3 = print_output(ftds, -1, -2, -3)
out4 = print_output(ftds, 'a','b','c')
print(out1, out2, out3, out4)
which isn’t exactly what I imagine as an easy way of getting what I want.
Function Decorators & Syntactic Sugar
Fortunately, Python has a syntactic sugar to express this logic. By using @decorate_name
at the top of a function declaration, you can express equivalent logic in an easy way decorate_name(func)
:
def capture_output(func_example):
def wrapper():
print(func_example())
return wrapper
@capture_output
def return10():
return 10
capture_output(return10)() # equivalent expression as above
Our Toy Example:
Now instead of writing the pattern here:
out1 = function_that_does_something(1,2,3)
out2 = function_that_does_something(0,0,0)
out3 = function_that_does_something(-1,-2,-3)
out4 = function_that_does_something('a','b','c')
print(out1, out2, out3, out4)
>> ('foo', 'bar', 'baz', 'fizz')
we can do this:
import functools
# print the output of func
def print_output(func):
@functools.wraps(func)
def pout(*args, **kwargs):
output = func(*args, **kwargs)
print(f"{func.__name__} output = {output}")
return output
return pout
@print_output
def function_that_does_something(index, str1, str2):
# do something
out1 = function_that_does_something(1,2,3)
out2 = function_that_does_something(0,0,0)
out3 = function_that_does_something(-1,-2,-3)
out4 = function_that_does_something('a','b','c')
>> 'function_that_does_something output = foo'
>> 'function_that_does_something output = bar'
>> 'function_that_does_something output = baz'
>> 'function_that_does_something output = fizz'
Actual Examples:
If I have a model that takes a long time to train, I send a notification to my email when the function is done running like this:
# Decorator that calls endExec after func is finished running
def post_exec(endExec):
if(not callable(endExec)):
warnings.warn("The argument is not callable")
def decorator_execute(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
output = func(*args, **kwargs)
endExec()
return output
return wrapped
return decorator_execute
def send_email(username, pw, api_key, msg):
sess = authenticate(username, pw, api_key)
sess.send_email(msg)
@dec.post_exec(lambda : send_email('username', 'password', 'api_key', 'done'))
def train():
# Do something for a long time
train() # will send an email when done
Conclusion
Feel free to use these if you think they can benefit you. Github gist is right here. Maybe at a comment if this helped.