【问题标题】:Get function name when ContextDecorator is used as a decoratorContextDecorator用作装饰器时获取函数名
【发布时间】:2021-12-31 16:49:17
【问题描述】:

我有以下上下文管理器和装饰器来为任何给定的函数或代码块计时:

import time
from contextlib import ContextDecorator


class timer(ContextDecorator):
    def __init__(self, label: str):
        self.label = label

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

    def __exit__(self, *exc):
        net_time = time.perf_counter() - self.start_time
        print(f"{self.label} took {net_time:.1f} seconds")
        return False

您可以将其用作上下文管理器:

with timer("my code block"):
    time.sleep(2)

# my code block took 2.0 seconds

您也可以将其用作装饰器:

@timer("my_func")
def my_func():
    time.sleep(3)

my_func()

# my_func took 3.0 seconds

我唯一不喜欢的是在用作装饰器时必须手动将函数名称作为label 传递。如果没有传递标签,我希望装饰器自动使用函数名称:

@timer()
def my_func():
    time.sleep(3)

my_func()

# my_func took 3.0 seconds

有什么办法吗?

【问题讨论】:

    标签: python decorator python-decorators


    【解决方案1】:

    如果您还覆盖了从 timer 类中的 ContextDecorator base class 继承的 __call__() 方法,并为 label 参数的初始化程序添加一个唯一的默认值,您可以检查它并调用函数时获取函数的__name__

    import time
    from contextlib import ContextDecorator
    
    
    class timer(ContextDecorator):
        def __init__(self, label: str=None):
            self.label = label
    
        def __call__(self, func):
            if self.label is None:  # Label was not provided
                self.label = func.__name__  # Use function's name.
            return super().__call__(func)
    
        def __enter__(self):
            self.start_time = time.perf_counter()
            return self
    
        def __exit__(self, *exc):
            net_time = time.perf_counter() - self.start_time
            print(f"{self.label} took {net_time:.1f} seconds")
            return False
    
    
    @timer()
    def my_func():
        time.sleep(3)
    
    my_func()  # -> my_func took 3.0 seconds
    
    

    【讨论】:

      【解决方案2】:

      根据对ContextDecoratorsource 的检查,似乎无法将包装函数的名称提供给上下文管理器。相反,您可以创建自己的ContextDecorator 版本,覆盖__call__

      import time
      import functools, contextlib
      class _ContextDecorator(contextlib.ContextDecorator):
         def __call__(self, func):
            self.f_name = func.__name__
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                with self._recreate_cm():
                   return func(*args, **kwargs)
            return wrapper
      

      用法:

      class timer(_ContextDecorator):
         def __init__(self, label: str = None):
            self.f_name = label
         def __enter__(self):
            self.start_time = time.perf_counter()
            return self
         def __exit__(self, *exc):
            net_time = time.perf_counter() - self.start_time
            print(f"{self.f_name} took {net_time:.1f} seconds")
            return False
      
      with timer('my_func'):
         time.sleep(2)
      
      @timer()
      def my_func():
         time.sleep(3)
      
      my_func()
      

      【讨论】:

      • 您不需要单独定义自己的ContextDecorator 版本来覆盖__call__() 方法。只需在已定义的子类中执行此操作(就像我在 my answer 中所做的那样)。
      • @martineau 我认为将组件分开更有意义。此类行为预计由ContextDecorator 提供,而不是timer。事实上,如果首先需要这种可定制性,那么它可能应该在timer 中完整实现,而不是ContextDecorator
      • 我不同意将它们分开,因为我认为 _ContextDecorator 所做的事情几乎不需要在其他情况下重复使用。将它们组合成一个类可以更好地封装正在发生的事情。
      猜你喜欢
      • 2021-10-31
      • 2020-12-18
      • 1970-01-01
      • 2023-01-21
      • 1970-01-01
      • 2011-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多