挂钩Python中的每个函数调用

Inf*_*ity 0 python hook metaprogramming python-3.x

我有一个大型代码库,其中包含数千个功能。

我想在每个函数调用之前和之后,函数启动时和结束时启用代码执行。

有没有一种方法无需重新编译Python或向每个函数添加代码?有没有办法钩住代码中的每个函数调用?

Mar*_*ers 8

是的,你可以使用的sys.settrace()或者sys.setprofile()函数注册一个回调和手柄'call',也许'return'事件。但是,这会大大降低您的代码速度。调用函数会产生开销,为每个函数调用添加另一个函数调用会增加开销。

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

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

在这两种情况下,都将为您提供当前,以及事件名称和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)
Run Code Online (Sandbox Code Playgroud)

当使用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")
Run Code Online (Sandbox Code Playgroud)

运行此输出:

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)
Run Code Online (Sandbox Code Playgroud)

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

  • @Hossein,你必须为每个线程调用 sys.set 跟踪;请参阅我的答案链接的函数文档。 (2认同)