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

Python context manager that measures time

提问人:ArekBulski 提问时间:11/30/2015 更新时间:8/24/2023 访问量:19330

问:

我正在努力制作一段代码,允许测量在“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

这段代码留下了 clock() 调用之间的差异。如何处理这个问题?我需要一种方法从退出方法中分配一个新值。t=1

PEP 343 更详细地描述了 contect manager 的工作原理,但我不理解其中的大部分内容。

python with-语句

评论

0赞 haccks 9/12/2021
@Bhargav饶;如果您认为这是一个骗局,那么为什么不将其作为骗局关闭呢?

答:

10赞 ArekBulski 11/30/2015 #1

解决了(几乎)。生成的变量是可强制的,可以转换为浮点数(但不是浮点数本身)。

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

评论

0赞 tdelaney 11/30/2015
catchtime不能是 a,因为浮点数是不可变的,但您希望在上下文退出时更改它。您可以实现 float 接口(参见 )并(大部分)像 float 一样工作,但即便如此,当需要不可变对象(如键)时,您也会遇到问题。floatdir(float)dict
0赞 ArekBulski 11/30/2015
我认为对浮动的胁迫是最接近理想的。
16赞 BrenBarn 11/30/2015 #2

你不能让它把你的时间分配给 。如 PEP 中所述,您在子句中指定的变量(如果有)被分配调用的结果,而不是 。换句话说,只在块的开头分配,而不是在块的末尾分配。tas__enter____exit__twith

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

要做到这一点,您还需要返回而不是 from 。不确定要通过使用 来实现什么。self1__enter__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 的值。

评论

0赞 BrenBarn 11/30/2015
@ArekBulski:就像我说的,这是不可能的。该行中的变量仅赋值一次,位于 的开头。这在 PEP 中有所描述。该语句旨在让上下文管理器管理块发生的代码的上下文。您可以摆弄上下文管理器并在其上存储数据,但它不应该能够以您似乎想要的方式修改周围环境。aswithwithwith
0赞 BrenBarn 11/30/2015
@ArekBulski:你能解释一下为什么这方面对你如此重要吗?无论你要做什么,就去做吧。这没什么大不了的。tt.t
1赞 ArekBulski 11/30/2015
它看起来更漂亮:)一半的 Python 习语是关于让事情变得漂亮。
1赞 user66081 6/23/2020
你可以有一处房产,或者,我认为那更漂亮.seconds.total_seconds
31赞 Vlad Bezden 7/17/2020 #3

下面是一个使用 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赞 Evgeny 5/1/2021
也使用 perf_counter(),这里比 time.clock() 更好。为什么这不在标准库中?
0赞 acmpo6ou 5/19/2021
这个例子来自“20 个你没有使用(但应该)的 Python 库”一书吗?
2赞 Mercury 7/9/2021
虽然不完全是 OP 要求的,但我可以做一些类似的事情,对吧?我一直在寻找一个 CM,它可以打印出在其中执行内容所需的时间,这看起来很完美。start = perf_counter(); yield; print(perf_counter()-start);
2赞 Justin Dehorty 9/13/2021
@Mercury是对的。此答案不正确,因为生成的值也可能受到语句外部代码的影响。例如,如果你在语句之后、语句之前调用 time.sleep(5),那么将给你大约 6 秒的时间,而不是 1 秒。t()withwithprintt()
38赞 Justin Dehorty 9/13/2021 #4

这里其他最受好评的答案可能是不正确的

正如 @Mercury 所指出的,@Vlad Bezden 的另一个最佳答案虽然很巧妙,但在技术上是不正确的,因为 产生的值也可能受到语句之外执行的代码的影响。例如,如果在语句之后但在语句之前运行,则调用 print 语句将为您提供 ~6 秒,而不是 ~1 秒。t()withtime.sleep(5)withprintt()

在某些情况下,可以通过在上下文管理器中插入 print 命令来避免这种情况,如下所示:

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:上下文管理器方法(带修复)

此解决方案使用两个参考点和 来捕获时差。通过确保仅在上下文管理器退出时更新,即使阻塞后出现延迟或操作,上下文中的经过时间也会保持一致。t1t2t2with

其工作原理如下:

  • 进入阶段:当进入上下文管理器时,两者都使用当前时间戳进行初始化。这确保了它们的差值最初为零。t1t2

  • 在上下文中:在此阶段没有更改或发生任何更改。因此,它们的差异保持为零。t1t2

  • 退出阶段:仅在上下文管理器退出时更新为当前时间戳。此步骤“锁定”了结束时间。然后,差值仅表示上下文中经过的时间。t2t2 - 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

  • 原始运行时间:以秒为单位的运行时间(以 )为单位存储,以备将来使用或计算。floatself.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"

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

3赞 Vitaly Sedelnik 10/17/2021 #5

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

@contextmanager
def catchtime() -> float:
    start = perf_counter()
    end = start
    yield lambda: end - start
    end = perf_counter()
2赞 pouya 11/18/2021 #6

您可以通过以下方式进行操作:

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”语句中处理代码所花费的时间。

评论

0赞 waykiki 8/7/2023
这是最干净、最蟒蛇的方式。
5赞 Greg 1/20/2022 #7

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

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
1赞 nik 11/22/2022 #8

通过此实现,您可以在该过程中和之后的任何时间获得时间

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