【问题标题】:Python context manager that measures time测量时间的 Python 上下文管理器
【发布时间】:2024-05-29 11:50:02
【问题描述】:

我正在努力编写一段代码,允许测量“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 更详细地了解联系人管理器的工作原理,但我大部分都不懂。

【问题讨论】:

  • @Bhargav Rao;如果您认为这是一个骗子,那么为什么不将其作为骗子关闭呢?

标签: python with-statement


【解决方案1】:

你可以在下面这样操作:

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      评分最高的答案可能会给出错误的时间

      正如@Mercury 已经指出的那样,@Vlad Bezden 的最佳答案虽然巧妙,但在技术上是不正确的,因为t() 产生的值也可能受到在with 语句之外执行的代码的影响。例如,如果您在 with 语句之后但在 print 语句之前执行 time.sleep(5),那么在 print 语句中调用 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"
      

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 也使用 perf_counter(),这里比 time.clock() 更好。为什么标准库中没有这个?
        • 这个例子来自“你不使用(但应该)的 20 个 Python 库”一书吗?
        • 虽然不是 OP 所要求的,但我可以做类似start = perf_counter(); yield; print(perf_counter()-start); 的事情,对吧?我一直在寻找一个 CM,它可以打印出执行其中的内容所需的时间,这看起来很完美。
        • @Mercury 是对的。这个答案是不正确的,因为t() 产生的值也可能受到with 语句之外的代码的影响。例如,如果您在 with 语句之后但在 print 语句之前调用 time.sleep(5),那么 t() 将给您大约 6 秒,而不是 1 秒。
        【解决方案5】:

        解决了(几乎)。结果变量是可强制转换的并且可以转换为浮点数(但不是浮点数本身)。

        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 接口(参见 dir(float))并(大部分)像 float 一样工作,但即便如此,当需要 dict 键等不可变对象时也会遇到问题。
        • 我认为对浮点数的强制是最接近理想的。
        【解决方案6】:

        您无法将您的时间分配给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内部 的代码的上下文。您可以摆弄上下文管理器并在其上存储数据,但它不应该能够以您想要的方式修改周围的环境。
        • @ArekBulski:你能解释一下为什么这方面对你来说如此重要吗?无论您打算使用t 做什么,只需使用t.t 即可。这没什么大不了的。
        • 看起来更漂亮 :) 一半的 Python 习语是为了让事情变得漂亮。
        • 你可以拥有.seconds.total_seconds的属性,我觉得这样更漂亮