Python context manager that measures time

Solution 1:

Here is an example of using contextmanager

from time import perf_counter
from contextlib import contextmanager

@contextmanager
def catchtime() -> float:
    start = perf_counter()
    yield lambda: perf_counter() - start


with catchtime() as t:
    import time
    time.sleep(1)

print(f"Execution time: {t():.4f} secs")

Output:

Execution time: 1.0014 secs

Solution 2:

You can't get that to assign your timing to t. As described in the PEP, the variable you specify in the as clause (if any) gets assigned the result of calling __enter__, not __exit__. In other words, t is only assigned at the start of the with block, not at the end.

What you could do is change your __exit__ so that instead of returning the value, it does self.t = time.clock() - self.t. Then, after the with block finishes, the t attribute of the context manager will hold the elapsed time.

To make that work, you also want to return self instead of 1 from __enter__. Not sure what you were trying to achieve by using 1.

So it looks like this:

class catchtime(object):
    def __enter__(self):
        self.t = time.clock()
        return self

    def __exit__(self, type, value, traceback):
        self.t = time.clock() - self.t

with catchtime() as t:
    time.sleep(1)

print(t.t)

And a value pretty close to 1 is printed.

Solution 3:

Solved (almost). Resulting variable is coercible and convertible to a float (but not a float itself).

class catchtime:
    def __enter__(self):
        self.t = time.clock()
        return self

    def __exit__(self, type, value, traceback):
        self.e = time.clock()

    def __float__(self):
        return float(self.e - self.t)

    def __coerce__(self, other):
        return (float(self), other)

    def __str__(self):
        return str(float(self))

    def __repr__(self):
        return str(float(self))

with catchtime() as t:
    pass

print t
print repr(t)
print float(t)
print 0+t
print 1*t

1.10000000001e-05
1.10000000001e-05
1.10000000001e-05
1.10000000001e-05
1.10000000001e-05

Solution 4:

The top rated answer can give the incorrect time

As noted already by @Mercury, the top answer by @Vlad Bezden, while slick, is technically incorrect since the value yielded by t() is also potentially affected by code executed outside of the with statement. For example, if you execute time.sleep(5) after the with statement but before the print statement, then calling t() in the print statement will give you ~6 sec, not ~1 sec.

This can be avoided by doing the print command inside the context manager as below:

from time import perf_counter
from contextlib import contextmanager


@contextmanager
def catchtime() -> float:
    start = perf_counter()
    yield lambda: perf_counter() - start
    # Note: print is included here to guarantee only time of code inside the CM is measured
    print(f'Time: {perf_counter() - start:.3f} seconds')

Notice how sleep(5) causes the incorrect time to be printed:

from time import sleep

with catchtime() as t:
    sleep(1)

# >>> "Time: 1.000 seconds"

sleep(5)
print(f'Time: {t():.3f} seconds')

# >>> "Time: 6.000 seconds"

A more robust solution (recommended)

This code is similar to the excellent answer given by @BrenBarn execept that it:

  1. Automatically prints the executed time as a formatted string (remove this by deleting the final print(self.readout))
  2. Saves the formatted string for later use (self.readout)
  3. Saves the float result for later use (self.time)
from time import perf_counter


class catchtime:
    def __enter__(self):
        self.time = perf_counter()
        return self

    def __exit__(self, type, value, traceback):
        self.time = perf_counter() - self.time
        self.readout = f'Time: {self.time:.3f} seconds'
        print(self.readout)

Notice how the intermediate sleep(5) commands no longer have an effect on the printed time.

from time import sleep

with catchtime() as t:
    sleep(1)

# >>> "Time: 1.000 seconds"

sleep(5)
print(t.time)

# >>> 1.000283900000009

sleep(5)
print(t.readout)

# >>> "Time: 1.000 seconds"