Create a Timer in Python: Elapsed Time, Decorators, and more
In this short guide, we will explore different timer implementations in Python.
In short, we can make a simple timer with Python's
time builtin library like so:
Although very straightforward, this is not the best option for timing the performance of your code.
For the rest of this article, we will expand on this concept by calculating and reporting a function's execution time.
Marking a Point in Time: Python's time Module
time module is equipped with many methods to report the GMT or the local time nicely formatted in years, months, days, hours, minutes, and seconds.
For our timing purposes, it's beneficial to have time represented as a single number. We achieved this in the intro code, but here's what the
time() function actually outputs:
The number printed on the screen represents the time in mere seconds passed since some system-dependent epoch (most systems use January 1st, 1970), stripped from any other construct.
Since we have time reduced to a floating-point number, we can just subtract one point in time from the other.
Let's bring our example back from the intro:
Here, we have used
time.sleep(10) to suspend the execution for ten seconds. We have marked the time before and after
time.sleep(), calculated the elapsed time, and printed it out.
The result is a little over ten seconds and is a slightly different number at each execution. This is because timing depends on other system activities—the system runs random operations in between, driving us over ten seconds.
Better alternatives to time()
Since we have roughly established how to calculate the elapsed time, we can refine our code a little. Python's time module offers us alternatives to the
time() function, all reporting the time in seconds but in different ways.
time() we have employed up until now reports the system's wall-clock time. We also have a function named
monotonic() at our disposal. This function uses a monotonic clock that cannot go backward or be affected by system clock updates.
If we were planning on measuring a long-running process, we would opt for
monotonic() for its integrity, but we will be measuring simple procedures here.
Python provides us access to a performance counter for short time measurements, called
perf_counter(), which uses the system clock with the highest resolution. Moving on, we will employ
perf_counter() in our calculations.
Timers using context managers
Context managers let us wrap logic with additional setup and teardown code, allowing us to manage resources efficiently. Since we intend to surround code with timing logic, context managers are a perfect fit.
Let's first look at a simple example of a context manager to better understand its structure and use.
demonstrate_cm is implemented as a context manager using
@contextmanager decorator. The
yield keyword divides the function body into two parts: 1) The expressions above
yield are executed right before the code that is managed 2) The expressions below
yield are executed right after.
Once defined, we attach
demonstrate_cm to a code block using a
In the next section, we will use the same structure to create a timer.
Defining a timer context manager
The timer context manager below is very similar to the context manager above, except now we are surrounding the
yield with our timing logic.
Let's quickly review this code.
First, notice that we have placed
yield within a
try-finally block to ensure the framed code block won't affect the context manager's job. If the managed code creates an error, the timer will still report the elapsed time until the error occurred.
try-finally, we have marked the starting point: the
finally, we have marked the endpoint and calculated the elapsed time. In the end, we have restricted the representation of
elapsed to four decimal points via an f-string and printed it out.
Let's put it to use.
One advantage of using the
@contextmanager decorator to define our timer is that we can use the timer both using the
with statement and as a function decorator, like in the last example. If we wanted to time a certain operation once, we could call in
timer() using a
with statement. However, if we wanted to track a function's run time at every execution, we can decorate the function using
What if we want to pass arguments to our context manager? We'll explore this idea in the next section.
Parameterizing the Timer
We have managed to program a timer that can be applied to any function. It is neat and portable. Yet, we can still go one step further and add some flexibility to it.
In the following code block, we've created a parameterized version of our timer, called
timer2. We have removed the hard-coded methods of accessing and reporting the time and allowed a
message parameter to format the message string.
with statement, we passed two arguments: (1) the timer function to use and a custom message for the function to print. Since we're using
process_time() as our timing function, we'll measure the CPU time, excluding the time spent sleeping.
In the next section, we'll work through converting our timer into a decorator, making it easier to apply timing capabilities to our functions.
Defining a Timer Decorator
We could also define our timer as a run-off-the-mill decorator. We'll design this implementation of our timer for recording an average run time for a given function.
In the code below, we're creating a standard decorator that takes the function to be decorated as its parameter. The nested timer is essentially like the previous timers we've created.
The timer code is almost the same as previous timers, but now we've added two variables for tracking time:
runs. These values will accumulate and hold the values needed to calculate the average.
Now, we'll apply this decorator to a function we wish to time:
When we decorate
@timer_wrapper, it is shorthand for writing
random_fn = timer_wrapper(random_fn), which masks the original definition of
random_fn to provide the additional functionality from the decorator. Note that
timer_wrapper() runs only once—it replaces
random_fn(), and its job is done.
timer() that is returned from
- Takes in
*args, **kwargs—these represent the arguments
funcmay or may not have.
- Records the time as
*args, **kwargs, and saves the result.
- Measures the elapsed time since
- Reaches out to the parent scope with
nonlocalto grab the variables
runs. The keyword
timer()is not creating new values here, but rather is reaching out to grab non-local ones.
- Adds one to
runs, adds the elapsed time to
- Prints the elapsed time -
- Prints the average run time -
- Returns the result from
funcback to the caller.
Overall, this is a helpful pattern for timing various functions in your code; add the decorator whenever your curious about performance and remove it when no longer needed.
Measuring the elapsed time inside a program can be accomplished by calling the
time.perf_counter() twice, once at the beginning and once at the end. The difference in the return values would then reveal the execution time.
This functionality can be programmed into a context manager, allowing us to measure the execution time of any code block using a
with statement. We can also implement it using a decorator and inject time measurement calls to the function that is decorated