I work a lot with computation times for my master's dissertation. During exploratory development I'm usually using iPython where the %timeit and %time magic commands usually serve my purpose. Their use is quite simple. The %time returns the elapsed time for the expression that follows. The %timeit magic command allows for control over how many times should the expression be executed in a loop, how many loops, precision of the result, among others. In short, it's closer in functionality to Python's timeit module.


In [1]:
%time a = [i**2 for i in range(10000)]


CPU times: user 3.88 ms, sys: 66 µs, total: 3.95 ms
Wall time: 3.5 ms

In [2]:
%timeit a = [i**2 for i in range(10000)]


1000 loops, best of 3: 1.38 ms per loop

There are two things I still would like to do though. The first is to store the times in variables. The second is that I often would like to time a specific method from a class without having to programmatically change the class. To address this, I created a simple stopwatch Python class based on the timeit module. I like Matlab's names for their stopwatch commands, so I based myself on them (I think tac is a better sound toc but that's another matter).


In [23]:
from timeit import default_timer as timer

class Timer():
    def __init__(self, ID = None):
        self.start = None
        self.end = None
        self.elapsed = 0

    def tic(self):
        self.start = timer()

    def tac(self):
        self.end = timer()
        self.elapsed += self.end - self.start
        return self.elapsed

    def reset(self):
        self.elapsed = 0
        
t = Timer()
t.tic()
a = [i**2 for i in range(10000)]
t.tac()


Out[23]:
0.004786014556884766

The second issue was more interesting and is actually the main reason I'm writing this post. Python's functions are just objects. I can passed them around as arguments if I want. This means it's quite easy to create a wrapper function around an input funtion. Here's an example of a wrapper function that will time the input function.


In [32]:
def wrapper_add(fn, *args, **kwargs):
    t = Timer()
    t.tic()
    returnvals = fn(*args, **kwargs)
    t.tac()
    return returnvals, t.elapsed

add = lambda x, y: x + y

wrapper_add(add, 1, 1)


Out[32]:
(2, 2.1457672119140625e-06)

My idea was then to generate a wrapper function that would time the input function. Since Python's functions are objects I can create them inside a funciton and then return them. So here is what I came up with:


In [ ]:
class Timer():
    # ... other methods ...
    def wrap_function(self, fn):
        def timed_fn(*args,**kwargs):
            self.tic()
            returnvals = fn(*args,**kwargs)
            self.tac()
            return returnvals
        return timed_fn

This method is taking in a function then creates a wrapper function like the one in the last example and returns it. It's that simple.

I can create a normal timer like in the above examples and then I can wrap a function with that specific timer. Whenever that function is executed it increments the timer with the elapsed time.


In [35]:
add = lambda x, y: x + y

t = Timer()
print t.elapsed

add_t = t.wrap_function(add)
add_t(2,2)
print t.elapsed

add_t = t.wrap_function(add)
add_t(2,2)
print t.elapsed


0
2.86102294922e-06
5.96046447754e-06

So I made a wrapper function generator but I still haven't changed an object's methods so they're timed as well. I'll show that now.


In [127]:
import numpy as np

class Dummy:
    def __init__(self, x, y):
        self.x = np.random.random(x)
        self.y = np.random.random(y)
        
    def _add(self):
        self.total = self.x + self.y
        return self.total
    
t1 = Timer()
t2 = Timer()

d = Dummy(10000000, 10000000)

t1.tic()
d._add()
t1.tac()
print d.total.sum(), t1.elapsed * 1000

d._add = t2.wrap_function(d._add)
d._add()
print d.total.sum(), t2.elapsed * 1000


10000114.2094 70.9750652313
10000114.2094 70.7581043243

There is a caveat to this, though. The object's attributes have to be writtable. If they're read-only then it's no play, as can be seen when I tried this on a NumPy array method.


In [130]:
import numpy as np

t = Timer()
arr = np.random.random(100000)

arr.sort = t.wrap_function(arr.argsort)


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-130-d8896a8a40e5> in <module>()
      4 arr = np.random.random(100000)
      5 
----> 6 arr.sort = t.wrap_function(arr.argsort)

AttributeError: 'numpy.ndarray' object attribute 'sort' is read-only