简体   繁体   English

用于跟踪/记录经过时间的优雅 Python 解决方案?

[英]Elegant Python solution for tracking/logging elapsed time?

With the goal of capturing meaningful elapsed-time information in for logs, I have replicated the following time-capture and logging code across many functions:为了在日志中捕获有意义的经过时间信息,我在许多函数中复制了以下时间捕获和日志记录代码:

import time
import datetime

def elapsed_str(seconds):
    """ Returns elapsed number of seconds in format '(elapsed HH:MM:SS)' """
    return "({} elapsed)".format(str(datetime.timedelta(seconds=int(seconds))))

def big_job(job_obj):
    """ Do a big job and return the result """
    start = time.time()
    logging.info(f"Starting big '{job_obj.name}' job...")
    logging.info(f"Doing stuff related to '{job_type}'...")
    time.sleep(10)  # Do some stuff...
    logging.info(f"Big '{job_obj.name}' job completed! "
                 f"{elapsed_str(time.time() - start)}")
    return my_result

With sample usage output:使用示例使用输出:

big_job("sheep-counting")
# Log Output:
#   2019-09-04 01:10:48,027 - INFO - Starting big 'sheep-counting' job...
#   2019-09-04 01:10:48,092 - INFO - Doing stuff related to 'sheep-counting'
#   2019-09-04 01:10:58,802 - INFO - Big 'sheep-counting' job completed! (0:00:10 elapsed)

I'm looking for an elegant (pythonic) method to remove these redundant lines from having to be rewritten each time:我正在寻找一种优雅的(pythonic)方法来删除这些多余的行,而不必每次都重写:

  1. start = time.time() - Should just automatically capture the start time at function launch. start = time.time() - 应该在函数启动时自动捕获开始时间。
  2. time.time() - start Should use previously captured start time and infer current time from now() . time.time() - start应该使用以前捕获的开始时间并从now()推断当前时间。 (Ideally elapsed_str() would be callable with zero arguments.) (理想情况下elapsed_str()可以使用零参数调用。)

My specific use case is to generate large datasets in the data science / data engineering field.我的具体用例是在数据科学/数据工程领域生成大型数据集。 Runtimes could be anywhere from seconds to days, and it is critical that (1) logs are easily searchable (for the word "elapsed" in this case) and (2) that the developer cost of adding the logs is very low (since we don't know ahead of time which jobs may be slow and we may not be able to modify source code once we identify a performance problem).运行时间可能从几秒到几天不等,关键在于 (1) 日志易于搜索(在本例中为“已过”一词)和 (2) 添加日志的开发人员成本非常低(因为我们不提前知道哪些作业可能会很慢,一旦我们发现性能问题,我们可能无法修改源代码)。

The recommended way is to use time.perf_counter() and time.perf_counter_ns() since 3.7.推荐的方法是从 3.7 开始使用time.perf_counter()time.perf_counter_ns()

In order to measure runtime of functions it is comfortable to use a decorator.为了测量函数的运行时间,使用装饰器很舒服。 For example the following one:例如下面的一个:

import time

def benchmark(fn):
    def _timing(*a, **kw):
        st = time.perf_counter()
        r = fn(*a, **kw)
        print(f"{fn.__name__} execution: {time.perf_counter() - st} seconds")
        return r

    return _timing

@benchmark
def your_test():
    print("IN")
    time.sleep(1)
    print("OUT")

your_test()

(c) The code of this decorator is slightly modified from sosw package (c) 这个装饰器的代码是从sosw包中稍微修改的

If I understood you correctly you could write a decorator that will time the function.如果我理解正确,您可以编写一个装饰器来为函数计时。

A good example here: https://stackoverflow.com/a/5478448/6001492这里有一个很好的例子: https : //stackoverflow.com/a/5478448/6001492

This may be overkill for others' use cases but the solution I found required a few difficult hurtles and I'll document them here for anyone who wants to accomplish something similar.这对于其他人的用例来说可能有点矫枉过正,但我​​发现的解决方案需要一些困难的麻烦,我会在此处为任何想要完成类似任务的人记录下来。

1. Helper function to dynamically evaluate f-strings 1. 动态评估 f-strings 的辅助函数

def fstr(fstring_text, locals, globals=None):
    """
    Dynamically evaluate the provided fstring_text

    Sample usage:
        format_str = "{i}*{i}={i*i}"
        i = 2
        fstr(format_str, locals()) # "2*2=4"
        i = 4
        fstr(format_str, locals()) # "4*4=16"
        fstr(format_str, {"i": 12}) # "10*10=100"
    """
    locals = locals or {}
    globals = globals or {}
    ret_val = eval(f'f"{fstring_text}"', locals, globals)
    return ret_val

2. The @logged decorator class 2. @logged 装饰器类

class logged(object):
    """
    Decorator class for logging function start, completion, and elapsed time.
    """

    def __init__(
        self,
        desc_text="'{desc_detail}' call to {fn.__name__}()",
        desc_detail="",
        start_msg="Beginning {desc_text}...",
        success_msg="Completed {desc_text}  {elapsed}",
        log_fn=logging.info,
        **addl_kwargs,
    ):
        """ All arguments optional """
        self.context = addl_kwargs.copy()  # start with addl. args
        self.context.update(locals())  # merge all constructor args
        self.context["elapsed"] = None
        self.context["start"] = time.time()

    def re_eval(self, context_key: str):
        """ Evaluate the f-string in self.context[context_key], store back the result """
        self.context[context_key] = fstr(self.context[context_key], locals=self.context)

    def elapsed_str(self):
        """ Return a formatted string, e.g. '(HH:MM:SS elapsed)' """
        seconds = time.time() - self.context["start"]
        return "({} elapsed)".format(str(datetime.timedelta(seconds=int(seconds))))

    def __call__(self, fn):
        """ Call the decorated function """

        def wrapped_fn(*args, **kwargs):
            """
            The decorated function definition. Note that the log needs access to 
            all passed arguments to the decorator, as well as all of the function's
            native args in a dictionary, even if args are not provided by keyword.
            If start_msg is None or success_msg is None, those log entries are skipped.
            """
            self.context["fn"] = fn
            fn_arg_names = inspect.getfullargspec(fn).args
            for x, arg_value in enumerate(args, 0):
                self.context[fn_arg_names[x]] = arg_value
            self.context.update(kwargs)
            desc_detail_fn = None
            log_fn = self.context["log_fn"]
            # If desc_detail is callable, evaluate dynamically (both before and after)
            if callable(self.context["desc_detail"]):
                desc_detail_fn = self.context["desc_detail"]
                self.context["desc_detail"] = desc_detail_fn()

            # Re-evaluate any decorator args which are fstrings
            self.re_eval("desc_detail")
            self.re_eval("desc_text")
            # Remove 'desc_detail' if blank or unused
            self.context["desc_text"] = self.context["desc_text"].replace("'' ", "")
            self.re_eval("start_msg")
            if self.context["start_msg"]:
                # log the start of execution
                log_fn(self.context["start_msg"])
            ret_val = fn(*args, **kwargs)
            if desc_detail_fn:
                # If desc_detail callable, then reevaluate
                self.context["desc_detail"] = desc_detail_fn()
            self.context["elapsed"] = self.elapsed_str()
            # log the end of execution
            log_fn(fstr(self.context["success_msg"], locals=self.context))
            return ret_val

        return wrapped_fn

Sample usage:示例用法:

@logged()
def my_func_a():
    pass
# 2019-08-18 - INFO - Beginning call to my_func_a()...
# 2019-08-18 - INFO - Completed call to my_func_a()  (00:00:00 elapsed)


@logged(log_fn=logging.debug)
def my_func_b():
    pass
# 2019-08-18 - DEBUG - Beginning call to my_func_b()...
# 2019-08-18 - DEBUG - Completed call to my_func_b()  (00:00:00 elapsed)


@logged("doing a thing")
def my_func_c():
    pass
# 2019-08-18 - INFO - Beginning doing a thing...
# 2019-08-18 - INFO - Completed doing a thing  (00:00:00 elapsed)


@logged("doing a thing with {foo_obj.name}")
def my_func_d(foo_obj):
    pass
# 2019-08-18 - INFO - Beginning doing a thing with Foo...
# 2019-08-18 - INFO - Completed doing a thing with Foo  (00:00:00 elapsed)


@logged("doing a thing with '{custom_kwarg}'", custom_kwarg="foo")
def my_func_e(foo_obj):
    pass
# 2019-08-18 - INFO - Beginning doing a thing with 'foo'...
# 2019-08-18 - INFO - Completed doing a thing with 'foo'  (00:00:00 elapsed)

Conclusion结论

The main advantages versus simpler decorator solutions are:与更简单的装饰器解决方案相比,主要优点是:

  1. By leveraging delayed execution of f-strings, and by injecting context variables from both the decorator constructor as well ass the function call itself, the log messaging can be easily customized to be human readable.通过利用 f 字符串的延迟执行,并通过从装饰器构造函数和函数调用本身注入上下文变量,日志消息可以轻松定制为人类可读。
  2. (And most importantly), almost any derivation of the function's arguments can be used to distinguish the logs in subsequent calls - without changing how the function itself is defined. (最重要的是),几乎任何函数参数的派生都可以用来区分后续调用中的日志——而无需改变函数本身的定义方式。
  3. Advanced callback scenarios can be achieved by sending a functions or complex objects to the decorator argument desc_detail , in which case the function would get evaluated both before and after function execution.高级回调方案可以通过将函数或复杂对象发送到装饰器参数desc_detail ,在这种情况下,函数将在函数执行之前和之后进行评估。 This could eventually be extended to use a callback functions to count rows in created data table (for instance) and to include the table row counts in the completion log.这最终可以扩展为使用回调函数来计算创建的数据表(例如)中的行数,并在完成日志中包含表行数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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