在Python中,如何在一个函数执行时获取它所调用的所有函数的列表?

编程语言 2026-07-11

我在尝试跟踪程序执行到某些时间点时被调用的函数。

这并非用于调试,而是为了评估我编写的函数在不同输入数据集下的使用情况。我正在编写把有向图转换成另一种格式的代码,因此我想跟踪在转换过程中每个节点/边经过时会调用哪些函数。

因为我需要的信息是按节点/边来看的,执行结束时得到的调用栈就不太理想。

我想要类似这样的格式:

def convert_node(node_info):

    # get the function that should be called from my library of ConversionFunctions
    node_conversion_fxn = getattr(ConversionFunctions, node_info['fxn_name'])

    # call the retrieved function 
    node_conversion_fxn(**node_info['arguments'])

    # TODO: get list of which functions in the ConversionFunctions library were called
    # (the functions can call each other, 
    # so just keeping track of the top-level retrieved function is not enough)

我发现的一些答案建议使用装饰器,但这给我带来了一些问题,因为我的代码的一部分需要检查 node_info['arguments'] 是否与 node_info['fxn_name'] 的函数签名匹配,而使用装饰器时,函数签名会因为被包装而改变。解决这个问题是一种可能的办法,但如果有其他实现方式也可以。

解决方案

这似乎是一个专门为原生、相对较新、轻量级监控框架(Python 3.12及以上)设计的任务:

(在较旧的Python版本中,通常需要诉诸设置跟踪,就像调试工具那样,这会带来更大的负担。)

我猜 sys.monitoring 的文档应该能自我说明。由于要进行几处调用来把一切设置好,这里给出一个“Hello World”示例,能够在函数调用发生的那一刻检测到所有调用。回调会把要调用的函数作为参数传入。(其他方法只是在函数被调用后提供当前的执行帧,需要将帧映射到代码对象再映射到函数名这一关系,且这一定关系并非天然成立,可能需要借助字典作为索引。)

import sys

TOOL_ID = 3

def callback(code, offset, callable, arg0):
    print(callable, arg0)

def callback(code, offset, callable, arg0):
    print(f"Function {callable.__name__} about to be called")
    # Events at target function have to be enabled at each call level

    # (Disclaimer - PoC code: assumes the callable is always a function and have a "__code__" directly)
    sys.monitoring.set_local_events(TOOL_ID, callable.__code__, sys.monitoring.events.CALL)

# enable the tool to use (1-5)
sys.monitoring.use_tool_id(3, "trace")

sys.monitoring.register_callback(TOOL_ID, sys.monitoring.events.CALL, callback)

# small function-calling tree for proof of concept:

def d(): pass
def c(): d()
def b(): pass
def a():
    b(); c()

# register the callback for the top-level function:
sys.monitoring.set_local_events(TOOL_ID, a.__code__, sys.monitoring.events.CALL)

if __name__ == "__main__":
    a()

备选方案

多亏评论中建议使用 @wraps 装饰器来在需要时保留原始参数名,我已经实现了一个可行的解决方案,但我不认为它是最理想的。

我把它作为一个答案发布,因为它能工作,同时希望一些评论能引导我找到更符合最佳实践的解决方案。

我有一个装饰器类,像这样:

from functools import wraps

class FxnTracer:
    # keep track of set of fxns called as a class attribute
    fxns_called = set()

    @staticmethod
    def __call__(f):
        @wraps(f)
        def fxn_tracker(*args, **kwargs):
            FxnTracer.fxns_called.add(f.__name__)
            return f(*args, **kwargs)
        return fxn_tracker

    # have a way to reset the set so it can be cleared out for each node/edge
    @staticmethod
    def reset_fxns_called():
        FxnTracer.fxns_called = set()

然后我在我的 ConversionFunctions 类中的所有函数都把它作为装饰器

class ConversionFunctions:
    @FxnTracer() # I think it's better to not have to have the parens, but I can't figure out how to make it work without them 
    def fxn1():
        return 

    @FxnTracer()
    def fxn2():
        return

然后 convert_to_node 函数的工作方式如下:

def convert_node(node_info, node_evaluation_obj):
    # reset tracer (i.e. should be done for each node conversion)
    FxnTracer.reset_fxns_called()

    # get the function that should be called from my library of ConversionFunctions
    node_conversion_fxn = getattr(ConversionFunctions, node_info['fxn_name'])

    # call the retrieved function 
    result = node_conversion_fxn(**node_info['arguments'])

    # add the called functions to eval obj 
    node_evaluation_obj.fxns_called.update(FxnTracer.fxns_called)
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章