Timing Code
Recently a colleague presented on ways to time code execution using time.perf_counter{}
to make informed decisions on code performance in terms of speed. This led me to think about creating some utilities that simplify the process. I've included examples of a context manager, a decorator and the timeit
function below:
Timing Code with a Context Manager
import random
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.perf_counter()
self.interval = self.end - self.start
def unique_nums_using_set(nums):
return list(set(nums))
def unique_nums_using_fromkeys(nums):
return list(dict.fromkeys(nums))
def main():
nums = [random.randint(0, 10) for _ in range(1000000)]
with Timer() as timer_one:
unique_nums = unique_nums_using_set(nums)
print(f"func = {unique_nums_using_set.__name__}")
print(f"Total = {unique_nums}")
print(f"Elapsed time: {timer_one.interval:0.4f} seconds")
with Timer() as timer_two:
unique_nums = unique_nums_using_fromkeys(nums)
print(f"func = {unique_nums_using_fromkeys.__name__}")
print(f"Total = {unique_nums}")
print(f"Elapsed time: {timer_two.interval:0.4f} seconds")
if __name__ == "__main__":
main()
# terminal output
func = unique_nums_using_set
Total = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Elapsed time: 0.0118 seconds
func = unique_nums_using_fromkeys
Total = [10, 3, 8, 9, 2, 6, 5, 4, 0, 1, 7]
Elapsed time: 0.0231 seconds
One thing I learnt while implementing the context manager is that you need to return self
from the __enter__
method to use the as
keyword. The below implementation shows another implementation without returning self
. Note in the main()
function that an instance of Timer
is instantiated and assigned to t
to be used with the with
keyword.
import random
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.perf_counter()
self.interval = self.end - self.start
def unique_nums_using_set(nums):
return list(set(nums))
def unique_nums_using_fromkeys(nums):
return list(dict.fromkeys(nums))
def main():
nums = [random.randint(0, 10) for _ in range(1000000)]
timer_one = Timer()
timer_two = Timer()
with timer_one:
unique_nums = unique_nums_using_set(nums)
print(f"func = {unique_nums_using_set.__name__}")
print(f"Total = {unique_nums}")
print(f"Elapsed time: {timer_one.interval:0.4f} seconds")
with timer_two:
unique_nums = unique_nums_using_fromkeys(nums)
print(f"func = {unique_nums_using_fromkeys.__name__}")
print(f"Total = {unique_nums}")
print(f"Elapsed time: {timer_two.interval:0.4f} seconds")
if __name__ == "__main__":
main()
# terminal output
func = unique_nums_using_set
Total = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Elapsed time: 0.0118 seconds
func = unique_nums_using_fromkeys
Total = [3, 4, 1, 6, 2, 10, 9, 5, 7, 8, 0]
Elapsed time: 0.0233 seconds
Timing Code with a Decorator
import random
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
func(*args, **kwargs)
end = time.perf_counter()
print(f'Elapsed time: {end-start:0.4f}')
return wrapper
@timer
def unique_nums_using_set(nums):
print(f"func = {unique_nums_using_set.__name__}")
return list(set(nums))
@timer
def unique_nums_using_fromkeys(nums):
print(f"func = {unique_nums_using_fromkeys.__name__}")
return list(dict.fromkeys(nums))
if __name__ == "__main__":
nums = [random.randint(0, 10) for _ in range(1000000)]
unique_nums_using_set(nums)
unique_nums_using_fromkeys(nums)
# terminal output
func = unique_nums_using_set
Elapsed time: 0.0119
func = unique_nums_using_fromkeys
Elapsed time: 0.0254
Using @wraps to return the original function name
The @wraps
decorator is required to return the wrapped functions name when calling unique_nums_using_set.__name__
or unique_nums_using_fromkeys.__name__
instead of returning wrapper
as the name. @wraps
updates the the metadata of the wrapper function to return the metadata of the original function.
Timing Code with timeit
As the timeit
function is used to measure small snippets of Python code, I've passed the expressions using set()
and dict.fromkeys
directly to the timeit
function.
import random
from timeit import timeit
nums = [random.randint(0, 10) for _ in range(1000000)]
set_time = timeit(
stmt="list(set(nums))", setup="from __main__ import nums", number=100
)
fromkeys_time = timeit(
stmt="list(dict.fromkeys(nums))", setup="from __main__ import nums", number=100
)
print(f"Time using set(): {set_time:.2f} seconds")
print(f"Time using dict.fromkeys(): {fromkeys_time:.2f} seconds")
# terminal output
Time using set(): 1.54 seconds
Time using dict.fromkeys(): 2.85 seconds
The statement I've passed to the timeit
setup
parameter seems clumsy. Another alternative is to use the globals
parameter to allow the timeit
function to access nums
.
set_time = timeit(
stmt="list(set(nums))", globals=globals(), number=100
)
fromkeys_time = timeit(
stmt="list(dict.fromkeys(nums))", globals=globals(), number=100
)