python - 在 Python 中 Hook 每个函数调用

标签 python python-3.x hook metaprogramming

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

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

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

最佳答案

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

默认情况下,sys.settrace() hook 仅在调用时调用(其中 call 表示正在输入一个新范围,包括类主体和列表、dict 和集合理解以及生成器表达式),但您可以选择返回一个跟踪函数以针对刚刚输入的范围调用.如果您只对通话感兴趣,则只需返回 None从跟踪功能。请注意,这使您可以选择收集更多信息的范围。 sys.settrace()仅报告 Python 代码,不报告内置可调用对象或已编译扩展中定义的可调用对象。
sys.setprofile()对 Python 函数和内置函数以及编译的扩展对象的调用都会调用 hook,并且每当调用返回或引发异常时,也会调用相同的回调。不幸的是,无法区分返回 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 或更高版本)。

这是两个钩子(Hook)的简短演示(假设使用 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有关如何执行此操作的更深入的文档。

关于python - 在 Python 中 Hook 每个函数调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59088671/

相关文章:

Python 静态 setter 和 getter?

python - python 中的内部类方法与实例方法

python - 尝试将 pandas 数据框保存到现有 Excel 工作表时出现 AttributeError

c++ - Winsock recv hooking with Detours

c++ - 使用代码注入(inject)在远程进程中执行函数

python - 为什么当我将一个字符串传递给 python 函数时,输出是两个字符串

python - 使用 AF_UNIX 套接字的双向通信

python - numpy 可以像 matlab 那样解释索引列吗

python - 将csv文件拆分为两个单独的文件python 3.7

iphone - Hook 到 C 函数