挂钩Python中的每个函数调用 [英] Hooking every function call in Python

查看:89
本文介绍了挂钩Python中的每个函数调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个大型的代码库,其中包含成千上万个函数.

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

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

解决方案

是的,您可以使用框架,以及事件名称和 arg ,通常设置为 None ,但对于某些事件,则需要更具体的设置:

  def call_tracer(frame,event,arg):#为每个新作用域调用,event ='call',arg = None#frame是框架对象,不是函数!print(f输入:{frame.f_code.co_name}")不返回sys.settrace(call_tracer) 

当使用 sys.settrace()返回函数对象而不是 None 时,可以跟踪框架中的其他事件,这是本地"跟踪函数.您可以为此重复使用相同的功能对象.这会进一步降低速度,因为现在您正在为源代码的每一行调用一个函数.然后为'line''exception''return'事件调用本地跟踪功能,但是您可以禁用每行事件通过设置 frame.f_trace_lines = False (需要Python 3.7或更高版本).

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

  import sys#演示函数,进行调用并返回东西def foo(bar,baz):返回栏(巴兹)def垃圾邮件(名称):print(f"Hello,{name}")返回[42 * i for i在范围(17)中]#跟踪函数,一个仅调用事件,另一个将调用和返回结合在一起def call_tracer(frame,event,arg):#为每个新作用域调用,event ='call',arg = None#frame是框架对象,不是函数!print(f输入:{frame.f_code.co_name}")不返回def call_and_return_tracer(frame,event,arg):如果event =='call':print(f输入:{frame.f_code.co_name}")#对于此新框架,仅跟踪异常并返回frame.f_trace_lines =假返回call_and_return_tracerelif event =='c_call':print(f输入:{arg .__ name__}")elif event =='return':print(f返回:{arg!r}")elif事件=='c_return':print(f返回自:{arg .__ name__}")如果__name__ =='__main__':sys.settrace(call_tracer)foo(垃圾邮件,世界")打印()sys.settrace(call_and_return_tracer)foo(垃圾邮件,"Universe")打印()sys.settrace(无)sys.setprofile(call_and_return_tracer)foo(垃圾邮件,个人资料") 

运行此输出:

 输入:foo输入:垃圾邮件你好,世界输入:< listcomp>输入:foo输入:垃圾邮件你好,宇宙输入:< listcomp>返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]输入:foo输入:垃圾邮件输入:打印您好,个人资料返回:打印输入:< listcomp>返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]返回:[0、42、84、126、168、210、252、294、336、378、420、462、504、546、588、630、672]返回:无 

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

I have a large codebase, which consists of thousands of functions.

I want to enable code execution before and after every function call, when the functions starts and when it ends.

Is there a way to do it without recompiling Python, or adding code to each function? Is there a way to hook every function call in my code?

解决方案

Yes, you can use either the sys.settrace() or sys.setprofile() functions to register a callback and handle 'call' and perhaps 'return' events. However, this can slow down your code considerably. Calling a function has overhead, adding another function call for every function call adds more overhead.

By default, the sys.settrace() hook is only called for calls (where call indicates a new scope being entered, including class bodies and list, dict and set comprehensions, as well as generator expressions), but you can optionally return a trace function to be called for the scope just entered. If you are only interested in calls then just return None from the trace function. Note that this lets you be selective about what scopes you gather more information about. sys.settrace() only reports on Python code, not built-in callables or those defined in compiled extensions.

The sys.setprofile() hook is called for calls to both Python functions and builtins and compiled extension objects, and the same callback is also called whenever a call returns or an exception was raised. Unfortunately it’s not possible to distinguish between a Python function returning None or raising an exception.

In both cases you are given the current frame, as well as the event name and arg, usually set to None but for some events to something more specific:

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)

When using sys.settrace() returning a function object instead of None lets you trace other events within the frame, this is the 'local' trace function. You can re-use the same function object for this. This slows things down more, because now you are calling a function for every line of source code. The local trace function is then called for 'line', 'exception' and 'return' events, but you can disable per-line events by setting frame.f_trace_lines = False (requires Python 3.7 or newer).

Here is a short demo of both hooks (assuming Python 3.7 or newer is used); it ignores the exception event option:

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")

Running this outputs:

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

If possible to alter the code, add decorators only to the functions you want to trace, so you can limit the overhead. You can even automate this if you are prepared to write some code to do the alterations; with the ast module you can parse code into object trees that can be transformed, including adding in @decorator syntax. This isn't that simple but really worth it if your codebase is large. See the Green Tree Snakes project for more in-depth documentation on how to do that.

这篇关于挂钩Python中的每个函数调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆