用于跟踪/记录运行时间的优雅Python解决方案?
为了在日志中捕获有意义的运行时间信息,我在许多函数中复制了以下时间捕获和日志代码:用于跟踪/记录运行时间的优雅Python解决方案?,python,logging,python-decorators,Python,Logging,Python Decorators,为了在日志中捕获有意义的运行时间信息,我在许多函数中复制了以下时间捕获和日志代码: 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)
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
使用示例输出:
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)
我正在寻找一种优雅的pythonic方法来消除每次重写这些冗余行:
start=time.time-应该在函数启动时自动捕获开始时间。
time.time-开始应使用以前捕获的开始时间,并从现在开始推断当前时间。理想情况下,RU str可以用零参数调用。
我的具体用例是在数据科学/数据工程领域生成大型数据集。运行时可以是几秒到几天,重要的是,1在本例中,可以轻松搜索日志中的“已用”一词;2添加日志的开发人员成本非常低,因为我们无法提前知道哪些作业可能会很慢,而且一旦发现性能问题,我们可能无法修改源代码。如果我理解正确,您可以编写一个将对函数计时的装饰器
这里有一个很好的例子:推荐的方法是从3.7开始使用time.perf_计数器和time.perf_计数器 为了度量函数的运行时,使用装饰器是比较合适的。例如,下面的一个:
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这个装饰器的代码从稍微修改了一下,这对于其他人的用例来说可能有些过分,但是我发现的解决方案需要一些困难,我将在这里为任何想要完成类似任务的人记录它们 1.用于动态计算f字符串的Helper函数 2.@logged decorator类 示例用法:
@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)
结论
与更简单的装饰解决方案相比,其主要优势是:
通过利用f字符串的延迟执行,并通过从decorator构造函数以及函数调用本身注入上下文变量,可以轻松地定制日志消息,使其具有可读性。
最重要的是,几乎任何函数参数的派生都可以用来区分后续调用中的日志,而不改变函数本身的定义方式。
高级回调场景可以通过将函数或复杂对象发送到decorator参数desc_detail来实现,在这种情况下,函数将在函数执行之前和之后进行求值。这最终可以扩展为使用回调函数对创建的数据表中的行进行计数,例如,在完成日志中包括表行计数。
非常感谢。这是非常接近我需要的!链接问题中似乎缺少的是自定义消息的功能。在本例中,这将是打印job_类型参数,但现实世界中的参数可能更复杂,只需打印全部或第一个参数。更现实的例子可能是包含name属性的字典或对象。更新了问题以调用job_obj.name而不是job_type,这在用例中更现实一些。通用方法打印函数名,这很有帮助,但它们似乎不允许日志消息本身具有灵活性。这很有帮助,但不幸的是,它不能像我的用例那样工作,因为这种方法不允许我自定义日志消息。具有不同参数的多个调用将创建彼此无法区分的日志。您可以在decorator中实现如何记录日志的逻辑。它可以访问函数对象以及使用args、kwargs调用它的所有上下文。您还可以在类内修饰函数,请参见我的链接中的原始示例。无论如何,这绝对是你要求的“蟒蛇式”方式。
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
@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)