16
\$\begingroup\$

This decorator adds the elapsed time to a function's attributes when applied.

My concerns:

  • Is the code pythonic?
  • Could this code be useful?
  • Should I just use the timeit module?
  • Is the code easy to read and understand?

The code:

'''
:Date: 7/21/17
:Version: 1
:Authors:
    - Ricky L Wilson
'''
 

import datetime

def time_func(function):
  """ This decorator calculates the amount of time a function takes to execute.
  
  When time_func is applied to a function it records how long the function takes to
  finish and add the elapsed time to the functions attributes.
  
  - **parameters**
  :param function: The function you want to add the elapsed time attribute to.
  
  :Example:
  @time_func  
  def example(name, **kwargs):
    meta = type(name, (object,), kwargs)
    return meta
    
  example('foo')
  print example.elapsed
  0:00:00.000052
  
  """
  def new_func(*args, **kwargs):
    # Start the clock.
    start = datetime.datetime.now()
    # Execute the function and record the results.
    function_result = function(*args, **kwargs)
    # Calculate the elapsed time and add it to the function
    # attributes.
    new_func.elapsed = datetime.datetime.now() - start
    # Returned the function with the added elapsed attribute 
    return function_result
  return new_func
    
\$\endgroup\$
1
  • 2
    \$\begingroup\$ ISO 8601 for the time stamp: 2017-07-21. \$\endgroup\$
    – Nat
    Jul 22, 2017 at 1:38

1 Answer 1

23
\$\begingroup\$

I'd recommend the timeit module when measuring the execution time of functions. AFAIK, timeit disables the garbage collector for the duration of the test, which might give you better results overall.

From here:

timeit is more accurate, for three reasons:

  • it repeats the tests many times to eliminate the influence of other tasks on your machine, such as disk flushing and OS scheduling.
  • it disables the garbage collector to prevent that process from skewing the results by scheduling a collection run at an inopportune moment.
  • it picks the most accurate timer for your OS, time.time or time.clock, see timeit.default_timer.

On the other side, a timing decorator is really useful because you can use annotations to sprinkle the timing around your code rather than making your code messy with timing logic everywhere. So yes, related to one of your questions, the code is useful.

Now, on the pythonic question, IMO:

  • you have too many comments which unfortunately didn't add any value to your code. Remove them.
  • your inner function could also be renamed to something more intuitive like wrapper.
  • function is also not the best choice when it comes to naming conventions as it might shadow the built-in function
  • use 2 newlines between imports and methods
  • the indentation should be a multiple of 4 (spaces)
  • use triple double quotes for your module docstring

In Python, there're already some useful modules to help you with this. For example, in functools you have the wraps decorator. This takes a function used in a decorator and adds the functionality of copying over the function name, docstring, arguments list, etc. And since wraps is itself a decorator I guess it'll make things a lot easier.


Code:

from functools import wraps
from time import time


def timing(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        start = time()
        result = f(*args, **kwargs)
        end = time()
        print 'Elapsed time: {}'.format(end-start)
        return result
    return wrapper

Usage:

@timing
def f(a):
    for _ in range(a):
        pass

print(f(2000000))

Result:

Elapsed time: 0.0971460342407

\$\endgroup\$
5
  • \$\begingroup\$ IMO it'd be good to be able to enable the GC. Also I think enabling the GC should be the default setting. \$\endgroup\$
    – Peilonrayz
    Jul 21, 2017 at 19:53
  • 2
    \$\begingroup\$ Well, timeit disables the GC to prevent \$X\$ process from skewing the results by scheduling a collection run at an inopportune moment. \$\endgroup\$ Jul 21, 2017 at 19:57
  • \$\begingroup\$ Hardly seems helpful trying to isolate the time of f to have to add the non-idempotent timing of GC. \$\endgroup\$ Jul 21, 2017 at 21:41
  • 4
    \$\begingroup\$ @ShawnMehan If you want to know the raw execution time of a function, ya, disabling GC may skew the time a bit. If you want to compare the execution times of different versions of a function however (which to me seems like the more likely case), eliminating as many of the sporadic factors as possible should make comparisons more accurate. You want to compate the difference between times, not the total possible duration of each call. \$\endgroup\$ Jul 21, 2017 at 22:35
  • \$\begingroup\$ Can add the function name to print statement so that it prints the name of the function as well along with the execution time \$\endgroup\$
    – ThePyGuy
    Jun 3, 2021 at 15:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.