Timing Functions With Decorators
In this lesson, you’ll see how to use decorators to measure the time a function takes to execute and print the duration to the console.
00:00
For this first example, I’ll have you create a decorator that can time functions and how long it takes for them to run. To start this process, back in the editor, inside the module decorators.py
, copy the boilerplate and paste it a little bit lower in the code.
00:23
Great. There is one more import that needs to happen for this decorator. Along with importing functools
, import—from the standard library—time
.
00:36
Now for your copied decorator, name it timer()
.
00:43
Just below it, using triple quotes ("""
) create the docstring…
00:52
and then end that also with triple quotes. So, timer()
takes a function into it, this is its docstring. Then it’s decorated with @functools.wraps()
. Instead of wrapper_decorator
, it’s going to be the timer
decorator, wrapper_timer()
.
01:08
It will accept *args
and **kwargs
.
01:13
And then something we’ll do here, you will assign a start_time
by using the method from time
perf_counter()
and calling it. I’ll leave a note here that it’s the first step that’s new outside of renaming.
01:31
So, that will happen before. Then, value = func(*args, **kwargs)
—that’ll stay the same. And then what’s going to happen after the function is called, is you will assign an end_time
,
01:44
using that same perf_counter()
method inside time
. Once the end_time
has been assigned, you can calculate a run_time
by taking the end_time
and subtracting the start_time
.
02:00
And let’s print to the console that it’s finished running your function by using the .__name__
method and add the flag to make sure that it uses the repr()
version of the name.
02:17
Close the curly brace. And then, inside that f-string, you also will say it ran in run_time
—as a float, with four places of precision—seconds.
02:31
Close off the print statement. Then, it will return any values from here that were calculated when the function was run, and then you need to return wrapper_timer
.
02:46
And save. Okay, so here it is: your decorator that will time functions, that you just built. You can remove this additional comment. Looks good! So, when the function is called, it’s passed into here, a start_time
is calculated, the function runs. When the function has been called and is completed, an end_time
is generated.
03:10
Then a run_time
is calculated by subtracting the start out of the end time, and then to the console is printed a statement f"Finished"
, our function’s .__name__
—and that’s good that we have our functools
—and then the run_time
, and then the value
is returned. Great! Okay. Again, make sure you have saved, then back in examples
, change your import
statement.
03:33
You’re not just importing the decorator @do_twice
, but importing all the decorators that have been created by using an asterisk (*
).
03:42
I’m going to save. Okay. Let me have you define a function that wastes a bit of time. Decorate it with @timer
, and as you defined it, it’s going to be called waste_some_time()
.
03:54
It takes in an argument of number of times, num_times
. And for _ in range()
, depending on how many number of times you’ve set, it will create a sum…
04:09
of all the squares for i
in the range up to 10000
. Go ahead and save. So now, you have a new function, waste_some_time()
, that’s decorated with your @timer
.
04:24
I’ll leave it showing here in the background. Okay. Time to waste some time. How to do that? Go ahead and re-enter into your REPL. Mine’s bpython
again. Yours could be just typing python
or python3
.
04:38
You need to import waste_some_time()
. To do that, from examples
, simply do that: import waste_some_time
. Please make sure that you’re still in the decorators/
folder that has these two modules inside of it. Okay.
04:53
It looks like that imported properly, it’s showing up here in my REPL. There it is: a function
and its memory location. Now, when it’s called with an argument of just the number 1
—1
time.
05:06
It will run—and run very quickly, as you can see. So, from the start time to the end time, it took 0.0026
seconds. Great! Let’s try something a bit longer.
05:28 Okay. That took a bit longer, almost two and a half seconds. Decorators are pretty advanced. It’s good to practice visually stepping through all the stages of your decorator. This next example will help with that even more.
LJIN Lab on Aug. 10, 2019
I’ve been timing my functions like a freaking caveman. I love you man.
David on May 7, 2020
I can’t spot my mistake! :( On Section 3.2 Both Moduloes in the same directory…
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer()
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
Ctrl+S Open a fresh REPL
$ bpython
bpython version 0.19 on top of Python 3.7.5 /usr/bin/python3
>>> from examples import waste_some_time
Traceback (most recent call last):
File "<input>", line 1, in <module>
from examples import waste_some_time
File "/home/david/core/examples.py", line 9, in <module>
@timer
File "/home/david/core/decorators.py", line 33, in timer
return wrapper_timer()
File "/home/david/core/decorators.py", line 28, in wrapper_timer
value = func(*args, **kwargs)
TypeError: waste_some_time() missing 1 required positional argument: 'num_times'
>>>
Other examples so far worked perfectly fine… Thanks in advance :)
David on May 7, 2020
Found the mistake (A good hint that I need to sleep LOL): When defining the decorator, instead of return the function object, I returned the function calling. instead of:
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer()
Do this:(last line)
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer # no "()" here, we need the object to
# be returned.
Pierre on June 20, 2020
Hi Christopher,
Thanks for the great tutorial. I never could wrap my head around the concept of decorators until this video series. This and the other course I just watched on generators were, alone, so well worth the cost of the subscription to Real Python. I particularly appreciate the pace. You seem to have found the sweet spot between too slow and too fast (at least for my level of Python literacy).
One question, I’ve never seen the phrasing for _ in range(n)
. Is the _
just a placeholder that can be used where you don’t need to reference what seems to amount to the index of the range?
Ricky White RP Team on June 20, 2020
That’s correct, Pierre. It’s often used in Python for variables that don’t need to be referenced again. If you find yourself using it inside of the for
loop later on, you should refactor and give the variable a more semantic name.
sndselecta on Jan. 7, 2024
func.__name__!r
Please explain the !r
Thx
Geir Arne Hjelle RP Team on Jan. 7, 2024
Hi sndselecta.
The !r specifies that the f-string should use repr()
to format the name instead of str()
which is the default. In practice, it adds quotes around the name.
You can learn more about repr()
and str()
at realpython.com/python-repr-vs-str/ We have more information about !r at realpython.com/python-f-strings/#using-an-objects-string-representations-in-f-strings
sndselecta on Jan. 7, 2024
Thank you Geir Arne Hjelle, quick question is it possible edit (crUd), a post?
Bartosz Zaczyński RP Team on Jan. 8, 2024
@sndselecta I’m afraid you can’t edit comments, unfortunately. I fixed your formatting if that’s what you were after.
Become a Member to join the conversation.
Alan Eng on May 7, 2019
Mind = blown. The real-world examples really solidify my knowledge! Thanks.