Python 测量时间的上下文管理器

Python context manager that measures time

我正在努力制作一段代码,允许测量在 "with" 语句中花费的时间并将测量的时间(浮点数)分配给 "with" 语句中提供的变量。

import time

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

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

with catchtime() as t:
    pass

此代码留下 t=1 而不是 clock() 调用之间的区别。如何解决这个问题?我需要一种从退出方法中分配新值的方法。

PEP 343 describes 更详细地介绍了 contect manager 的工作原理,但我不明白其中的大部分内容。

解决(差不多)。结果变量可强制转换为浮点数(但不是浮点数本身)。

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

您无法将时间分配给 t。如 PEP 中所述,您在 as 子句中指定的变量(如果有)将被分配调用 __enter__ 而不是 __exit__ 的结果。换句话说,t 仅在 with 块的 start 处赋值,而不是在末尾赋值。

你可以做的是改变你的 __exit__,这样它就不会 return 值,而是 self.t = time.clock() - self.t。然后,在 with 块完成后,上下文管理器的 t 属性将保存经过的时间。

要实现这一点,您还需要 return self 而不是 __enter__ 中的 1。不确定您试图通过使用 1.

实现什么

所以看起来像这样:

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)

并且打印出一个非常接近 1 的值。

下面是一个使用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")

输出:

执行时间:1.0014 秒

评分最高的答案给出的时间可能不正确

正如@Mercury 已经指出的那样,@Vlad Bezden 的最佳答案虽然圆滑,但在技术上是不正确的,因为 t() 产生的值也可能受到外部执行的代码的影响with 语句。例如,如果您在 with 语句之后但在 print 语句之前执行 time.sleep(5),那么在打印语句中调用 t() 将给您 ~6 秒,而不是 ~1秒

这可以通过在上下文管理器中执行打印命令来避免,如下所示:

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')

注意 sleep(5) 如何导致打印不正确的时间:

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"

更健壮的解决方案(推荐)

此代码类似于@BrenBarn 给出的出色答案,只是它:

  1. 自动将执行时间打印为格式化字符串(通过删除最后的 print(self.readout) 来删除它)
  2. 保存格式化字符串供以后使用(self.readout)
  3. 保存 float 结果供以后使用 (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)

注意中间 sleep(5) 命令如何不再对打印时间产生影响。

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"

评分最高的答案中的问题也可以修复如下:

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

您可以按照下面的方式进行:

import time

class Exectime:

    def __enter__(self):
        self.time = time.time()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.time = time.time() - self.time



with Exectime() as ext:
    <your code here in with statement>

print('execution time is:' +str(ext.time))

它将计算在 'with' 语句中处理代码所花费的时间。

我喜欢这种方法,它使用简单并且允许上下文消息:

from time import perf_counter
from contextlib import ContextDecorator

class cmtimer(ContextDecorator):
    def __init__(self, msg):
        self.msg = msg

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

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

这样使用:

with cmtimer('Loading JSON'):
    with open('data.json') as f:
        results = json.load(f)

输出:

Loading JSON took 1.577 seconds