【问题标题】:Hooking every function call in Python在 Python 中挂钩每个函数调用
【发布时间】:2020-03-24 02:23:50
【问题描述】:

我有一个庞大的代码库,其中包含数千个函数。

我想在每次函数调用之前和之后、函数开始和结束时启用代码执行。

有没有办法在不重新编译 Python 或向每个函数中添加代码的情况下做到这一点?有没有办法在我的代码中挂钩每个函数调用?

【问题讨论】:

标签: python python-3.x hook metaprogramming


【解决方案1】:

是的,您可以使用sys.settrace()sys.setprofile() 函数来注册回调并处理'call''return' 事件。但是,这会大大减慢您的代码速度。调用一个函数有开销,为每个函数调用添加另一个函数调用会增加更多开销。

默认情况下,sys.settrace() 钩子仅在调用时被调用(其中 call 表示正在输入一个新范围,包括类主体和列表、dict 和集合解析以及生成器表达式) ,但您可以选择返回要为刚刚输入的范围调用的跟踪函数。如果您只对调用感兴趣,那么只需从跟踪函数返回 None。请注意,这使您可以选择收集更多信息的范围。 sys.settrace() 仅报告 Python 代码,而不是内置可调用对象或已编译扩展中定义的那些。

sys.setprofile() 钩子在调用 Python 函数和内置函数以及编译的扩展对象时被调用,并且每当调用返回或引发异常时也会调用相同的回调。不幸的是,无法区分返回 None 或引发异常的 Python 函数。

在这两种情况下,您都会获得当前的frame,以及事件名称和arg,通常设置为None,但对于某些事件来说更具体:

def call_tracer(frame, event, arg):
    # called for every new scope, event = 'call', arg = None
    # frame is a frame object, not a function!
    print(f"Entering: {frame.f_code.co_name}")
    return None

sys.settrace(call_tracer)

当使用 sys.settrace() 返回函数对象而不是 None 时,您可以跟踪帧内的其他事件,这是“本地”跟踪函数。您可以为此重复使用相同的函数对象。这会使事情变得更慢,因为现在您正在为每一行源代码调用一个函数。然后为 'line''exception''return' 事件调用本地跟踪函数,但您可以通过设置 frame.f_trace_lines = False 禁用每行事件(需要 Python 3.7 或更高版本)。

这是两个钩子的简短演示(假设使用 Python 3.7 或更新版本);它忽略了异常事件选项:

import sys

# demo functions, making calls and returning things

def foo(bar, baz):
    return bar(baz)

def spam(name):
    print(f"Hello, {name}")
    return [42 * i for i in range(17)]

# trace functions, one only call events, another combining calls and returns

def call_tracer(frame, event, arg):
    # called for every new scope, event = 'call', arg = None
    # frame is a frame object, not a function!
    print(f"Entering: {frame.f_code.co_name}")
    return None

def call_and_return_tracer(frame, event, arg):
    if event == 'call':
        print(f"Entering: {frame.f_code.co_name}")
        # for this new frame, only trace exceptions and returns
        frame.f_trace_lines = False
        return call_and_return_tracer
    elif event == 'c_call':
        print(f"Entering: {arg.__name__}")
    elif event == 'return':
        print(f"Returning: {arg!r}")
    elif event == 'c_return':
        print(f"Returning from: {arg.__name__}")

if __name__ == '__main__':
    sys.settrace(call_tracer)
    foo(spam, "world")
    print()

    sys.settrace(call_and_return_tracer)
    foo(spam, "universe")
    print()
    sys.settrace(None)

    sys.setprofile(call_and_return_tracer)
    foo(spam, "profile")

运行此输出:

Entering: foo
Entering: spam
Hello, world
Entering: <listcomp>

Entering: foo
Entering: spam
Hello, universe
Entering: <listcomp>
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]

Entering: foo
Entering: spam
Entering: print
Hello, profile
Returning from: print
Entering: <listcomp>
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]
Returning: [0, 42, 84, 126, 168, 210, 252, 294, 336, 378, 420, 462, 504, 546, 588, 630, 672]
Returning: None

如果可以更改代码,请仅将装饰器添加到您要跟踪的函数中,这样您就可以限制开销。如果您准备编写一些代码来进行更改,您甚至可以自动执行此操作;使用ast module,您可以将代码解析为可以转换的对象树,包括添加@decorator 语法。这不是那么简单,但如果你的代码库很大,这真的很值得。请参阅Green Tree Snakes project,了解有关如何执行此操作的更深入的文档。

【讨论】:

  • 这可以在多线程环境中工作吗?