Reputation: 178
After coming across this excellent explanation about decorators, I realized decorators can be quite useful to display function execution times. My ultimate goal is to create a function
compare_functions(f1, f2)
that compares the execution times of two functions f1 and f2. As a start I designed this decorator:
def clean_exec_time(fnc):
def wrapper(*args):
t0 = time.perf_counter()
fnc(*args)
t1 = time.perf_counter()
print(t1-t0)
return wrapper
Which can be used perfectly in this way:
@clean_exec_time
def print_msg(msg):
print(msg)
Such that
print_msg('Hi')
# outputs:
# Hi
# 3.0109e-05
That's great. It get's tricky (for me) when I try to work my way to the ultimate goal.
@clean_exec_time
def compare_functions(f1, f2):
a = 2 #not sure what to put here
Now I see the following:
compare_functions(print_msg('Hi'), print_msg('See ya'))
# outputs
# Hi
# 3.0793e-05
# See ya
# 1.1291e-05
# 6.5010e-05
I have a few questions:
Upvotes: 0
Views: 536
Reputation: 531175
compare_functions
is the wrong function to decorate, because you want to time each of the functions being passed to compare_functions
, not compare_functions
itself.
I find it more useful to time code with a context manager.
import time
import contextlib
class TimeResult:
def __init__(self):
self._start = None
self._stop = None
def start(self):
self._start = time.perf_counter()
def stop(self):
self._stop = time.perf_counter()
@property
def result(self):
if self._start is None or self._stop is None:
return None
return self._stop - self._start
This is a simple class whose result
property is None
until after you've called both its start
and stop
methods (preferably in that order)
@contextlib.contextmanager
def timeit():
t = TimeResult()
t.start()
yield t
t.stop()
This creates a context manager that yields a TimeResult
object whose start
and stop
methods are called at on entry to and on exit from a with
statement, respectively.
def compare_functions(f1, f2):
with timeit() as t1:
f1()
print(f'Function 1 took {t1.result} seconds')
with timeit() as t2:
f2()
print(f'Function 2 took {t2.result} seconds')
compare_functions
takes two functions to call, and times each of them the the timeit
context manager. As each function completes, the elapsed time is reported.
Then
def print_msg(msg):
print(msg)
compare_functions(lambda: print_msg("hi"), lambda: print_msg("see ya"))
takes two functions to call and time.
Alternatively, you can pass the functions to time and the arguments separately, and let compare_functions
combine them.
def compare_functions(f_and_args_1, f_and_args_2):
f1, *args1 = f_and_args1
f2, *args2 = f_and_args2
with timeit() as t1:
f1(*args1)
print(f'{f1.__name__} took {t1.result} seconds')
with timeit() as t2:
f2(*args2)
print(f'{f2.__name__} took {t2.result} seconds')
compare_functions((print_msg, "hi"), (print_msg, "see ya"))
Upvotes: 1
Reputation: 16815
As discussed, one possibility is to use lambdas as arguments:
def compare_time(f1, f2):
t0 = time.perf_counter()
f1()
t1 = time.perf_counter()
f2()
t2 = time.perf_counter()
print(f"Time diff is {(t1 - t0) - (t2 - t1)} s")
compare_time(lambda: print_msg('Hi'), lambda: print_msg('See ya'))
This gives:
Hi
See ya
Time diff is 1.9200000000010875e-05 s
Unfortunately, lambda functions do not have information about the called function. If you need this, here is a hacky solution, that takes the strings of the functions calls:
def compare_time1(f1, f2):
t0 = time.perf_counter()
eval(f1)
t1 = time.perf_counter()
eval(f2)
t2 = time.perf_counter()
print(f"Time diff between {f1} and {f2} is {(t1 - t0) - (t2 - t1)} s")
compare_time1("print_msg('Hi')", "print_msg('See ya')")
This gives:
Hi
See ya
Time diff between print_msg('Hi') and print_msg('See ya') is 3.0000000000002247e-05 s
Another possibility is to separate function names and arguments - not so hacky, but also not very convenient:
def compare_time2(f1, args1, f2, args2):
t0 = time.perf_counter()
f1(*args1)
t1 = time.perf_counter()
f2(*args2)
t2 = time.perf_counter()
print(f"Time diff between {f1.__name__}({str(args1)[1:-1]}) and"
f" {f2.__name__}({str(args2)[1:-1]}) is {(t1 - t0) - (t2 - t1)} s")
compare_time2(print_msg, ['Hi'], print_msg, ['See ya'])
This gives:
Hi
See ya
Time diff between print_msg('Hi') and print_msg('See ya') is 8.000000000091267e-07 s
Upvotes: 1
Reputation: 178
For future reference, this is the what I've done (without decorators).
def compare_functions(f1, f2, reps=1):
text_trap = io.StringIO() #
sys.stdout = text_trap # These two lines avoid print statements
fnc1 = get_func(f1)
fnc2 = get_func(f2)
c1 = get_exec_time(fnc1, reps)
c2 = get_exec_time(fnc2, reps)
sys.stdout = sys.__stdout__ # turn print statement back on
if c1 > c2:
print('Function {0} is {1:.2f} times faster than function {2}'.format(1, c1/c2, 2))
if c2 > c1:
print('Function {0} is {1:.2f} times faster than function {2}'.format(2, c2/c1, 1))
Where
def get_func(fnc):
if not callable(fnc):
return lambda: fnc
else:
return fnc
def get_system_speed(): #used as benchmark
t0 = time.perf_counter()
do_calc() #arbritrary function
return time.perf_counter()-t0
def get_exec_time(fnc, reps=1):
ex_times = []
for i in range(reps):
c = get_system_speed()
t0 = time.perf_counter()
fnc()
ex_times.append((time.perf_counter()-t0)/c)
return np.average(ex_times)
This allows me to compare the execution times of two (lambda) functions, by executing each function reps times and benchmarking each run to the system speed at that moment. For example, using the print_msg function as above:
compare_functions(lambda: print_msg('hi'), lambda: print_msg('hi'), reps=10000)
# outputs
# Function 2 is 1.00 times faster than function 1
Thanks for the help and suggestions!
Upvotes: 0