简体   繁体   中英

Get function name when ContextDecorator is used as a decorator

I have the following context manager and decorator to time any given function or code block:

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

You can use it as a context manager:

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

# my code block took 2.0 seconds

You can also use it as a decorator:

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

my_func()

# my_func took 3.0 seconds

The only thing I don't like is having to manually pass the function name as the label when it's used as a decorator. I would love for the decorator to automatically use the function name if no label is passed:

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

my_func()

# my_func took 3.0 seconds

Is there any way to do this?

If you also override the __call__() method inherited from ContextDecorator base class in your class, and add a unique default value to the initializer for the label argument, you can check for that and grab the function's __name__ when it's called:

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

Based on an examination of ContextDecorator 's source , there does not seem to be any way the name of the wrapped function can be made available to the contextmanager. Instead, you can create your own version of ContextDecorator , overriding __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

Usage:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM