Python上下文管理器,用于测量时间

31

我正在努力编写一段代码,可以测量“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,但我并不完全理解其中的大部分内容。

@Bhargav Rao; 如果您认为这是一个重复问题,为什么不将其标记为重复? - haccks
8个回答

39

这里的其他评级较高的答案可能是不正确的

正如@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
    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"

解决方案#1:上下文管理器方法(修复版)

此解决方案使用两个参考点t1t2来捕获时间差异。通过确保只有当上下文管理器退出时t2才会更新,即使在with块后存在延迟或操作,上下文内的经过时间仍然保持一致。

以下是其工作原理:

  • 进入阶段:当进入上下文管理器时,同时用当前时间戳初始化t1t2。这确保它们的差异最初为零。

  • 在上下文中:此阶段不对t1t2进行任何更改。因此,它们的差异保持为零。

  • 退出阶段:只有当上下文管理器退出时,t2才会更新为当前时间戳。这一步“锁定”了结束时间。然后,t2 - t1表示仅在上下文内经过的时间。

from time import perf_counter
from time import sleep
from contextlib import contextmanager

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

with catchtime() as t:
    sleep(1)

# Now external delays will no longer have an effect:

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

# Output: "Time: 1.000 seconds"


使用这种方法,with块外的操作或延迟不会扭曲时间测量。与本页上其他最受欢迎的答案不同,这种方法引入了一种间接性,您可以在退出上下文管理器时明确捕获结束时间戳。这一步有效地“冻结”了结束时间,防止它在上下文之外更新。

解决方案#2:基于类的方法(灵活)

这种方法建立在@BrenBarn的想法基础上,但增加了一些可用性改进:

  • 自动计时打印输出:一旦上下文中的代码块完成,经过的时间将自动打印出来。要禁用此功能,可以删除print(self.readout)行。

  • 存储格式化输出:经过的时间以格式化字符串的形式存储在self.readout中,可以在任何后续时间检索和打印。

  • 原始经过时间:经过的时间以秒为单位(作为float)存储在self.time中,以供可能的进一步使用或计算。

from time import perf_counter

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

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

与解决方案#1相同,即使在上下文管理器之后有操作(如sleep(5)),也不会影响捕获的经过时间。

from time import sleep

with catchtime() as timer:
    sleep(1)

# Output: "Time: 1.000 seconds"

sleep(5)
print(timer.time)

# Output: 1.000283900000009

sleep(5)
print(timer.readout)

# Output: "Time: 1.000 seconds"

这种方法在访问和利用经过的时间方面提供了灵活性,既可以作为原始数据,也可以作为格式化的字符串。

31
以下是使用contextmanager的示例:

这是使用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秒


2
还使用了perf_counter(),这比time.clock()更好。为什么这不在标准库中呢? - Evgeny
这个例子是来自于《20个你没有使用过的Python库(但应该使用)》一书吗? - acmpo6ou
2
虽然不完全是 OP 所要求的,但我可以做类似于 start = perf_counter(); yield; print(perf_counter()-start); 的东西,对吧?我正在寻找一个能够打印出执行其内部内容所需时间的 CM,这看起来非常完美。 - Mercury
2
@Mercury 是正确的。这个答案是不正确的,因为t()返回的值也可能受到with语句之外的代码的影响。例如,如果你在with语句之后但在print语句之前调用time.sleep(5),那么t()将会给你大约6秒而不是1秒。 - Justin Dehorty

16
您不能将时间分配给t。根据PEP的描述,您在as子句中指定的变量(如果有)将被分配为调用__enter__的结果,而不是__exit__的结果。换句话说,t仅在with块的开头分配,而不是在结尾处。
您可以更改__exit__,使其不返回值,而是执行self.t = time.clock() - self.t。然后,在with块完成后,上下文管理器的t属性将保存经过的时间。
为使此操作可行,您还要从__enter__返回self而不是返回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的数值。


@ArekBulski:就像我说的那样,这是不可能的。在 as 行中的变量只会在 with 的开始时被赋值一次。这在 PEP 中有描述。with 语句旨在让上下文管理器管理 with 块内发生的代码的上下文。您可以操作上下文管理器并在其上存储数据,但不能以您想要的方式修改周围的环境。 - BrenBarn
@ArekBulski:你能解释一下为什么这方面对你如此重要吗?无论你打算用t做什么,都可以用t.t代替。这不是什么大问题。 - BrenBarn
1
它看起来更漂亮 :) Python 的习惯用法中有一半是关于让事情变得漂亮。 - ArekBulski
1
你可以使用属性.seconds.total_seconds,我认为后者更美观。 - user66081

10
解决了(几乎)。得到的变量可以被强制转换并转化为浮点数(但不是浮点数本身)。
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

catchtime 不能是 float,因为 float 是不可变的,但你想在上下文退出时更改它。你可以实现 float 接口(参见 dir(float)),并且工作方式(大多数情况下)类似于 float,但即使这样,在需要不可变对象(如 dict 键)时仍会遇到问题。 - tdelaney
我认为将其强制转换为浮点数是最接近理想的方法。 - ArekBulski

5

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

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

3

顶级答案中的问题也可以通过以下方式解决:

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

2
您可以按照以下方式进行操作:
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”语句内处理代码所花费的时间。


这是最干净、最符合Python风格的方式。 - waykiki

1

通过这个实现,您可以在进程中和之后的任何时间获取时间

from contextlib import contextmanager
from time import perf_counter


@contextmanager
def catchtime(task_name='It', verbose=True):
    class timer:
        def __init__(self):
            self._t1 = None
            self._t2 = None

        def start(self):
            self._t1 = perf_counter()
            self._t2 = None

        def stop(self):
            self._t2 = perf_counter()

        @property
        def time(self):
            return (self._t2 or perf_counter()) - self._t1

    t = timer()
    t.start()
    try:
        yield t
    finally:
        t.stop()
        if verbose:
            print(f'{task_name} took {t.time :.3f} seconds')

使用示例:

from time import sleep

############################

# 1. will print result
with catchtime('First task'):
    sleep(1)

############################

# 2. will print result (without task name) and save result to t object
with catchtime() as t:
    sleep(1)

t.time  # operation time is saved here

############################

# 3. will not print anyhting but will save result to t object
with catchtime() as t:
    sleep(1)

t.time  # operation time is saved here

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接