如何借助Jedi库生成Python函数的完整调用链?
如何用Jedi构建Python函数的完整调用链
核心结论
Jedi本身不直接支持递归获取完整调用链,但可以通过手动递归遍历调用者的方式实现这个功能。Jedi的Name对象提供了definitions和references方法,结合递归向上查找,就能构建多层调用链。
实现思路
要生成从顶层函数到目标函数的完整调用链,需按以下步骤操作:
- 精准定位目标函数的定义位置(比如
models.user.User.get_by_id的定义) - 查找所有直接调用该函数的位置(即第一层调用者,如
services.products.get_products_by_user_id) - 对每个调用者,递归查找调用它的上层函数,直到没有更上层的调用者(顶层函数,如
controllers.products.ProductList.get) - 整理递归结果,输出树形结构的调用链
修改后的脚本实现
基于你的现有脚本,以下是实现完整调用链的版本:
import os import jedi proj_base_path = '/path/to/the/project_folder' target_fn_name = 'get_by_id' target_module = 'models.user' # 明确目标函数所在模块,避免同名函数混淆 def get_file_paths(path): ex_path = os.path.expanduser(path) for root, dirs, files in os.walk(ex_path): for f_path in files: if os.path.splitext(f_path)[1] != '.py': continue yield os.path.join(root, f_path) def get_full_name(name): """生成函数/方法的完整限定名,如 controllers.products.ProductList.get""" if name.type in ('function', 'method'): # 处理类方法:模块名.类名.方法名 if name.parent() and name.parent().type == 'class': return f"{name.module_name}.{name.parent().name}.{name.name}" # 处理普通函数:模块名.函数名 return f"{name.module_name}.{name.name}" return name.name def find_callers(name, visited=None): """递归查找调用当前函数的所有上层函数""" if visited is None: visited = set() callers = [] # 获取当前函数的所有调用引用 references = name.references() for ref in references: # 跳过自身定义和重复节点 if ref in visited or ref.type not in ('function', 'method'): continue visited.add(ref) # 向上定位到调用当前函数的上层函数/方法 caller = ref.parent() while caller and caller.type not in ('function', 'method', 'class'): caller = caller.parent() if caller and caller not in visited: callers.append(caller) # 递归查找调用者的上层函数 callers.extend(find_callers(caller, visited)) # 去重后返回 return list({c for c in callers}) def build_call_chain(target_name): """构建从顶层到目标函数的完整调用链""" call_chain = [] all_callers = find_callers(target_name) def traverse(caller, current_chain): new_chain = current_chain + [caller] # 查找调用当前caller的上层函数 upper_callers = [c for c in all_callers if caller in find_callers(c, set())] if not upper_callers: # 已到顶层函数,添加完整链(顶层->...->目标) call_chain.append(new_chain + [target_name]) else: for upper in upper_callers: traverse(upper, new_chain) # 先处理直接调用目标函数的节点 direct_callers = [c for c in all_callers if target_name in find_callers(c, set())] for caller in direct_callers: traverse(caller, []) return call_chain def print_call_chain(chain): """打印树形结构的调用链""" for i, node in enumerate(chain): indent = ' ' * i if i == 0: print(f"* {get_full_name(node)}") else: print(f"{indent}|") print(f"{indent} `---> * {get_full_name(node)}") print() if __name__ == '__main__': # 第一步:定位目标函数的定义 target_def = None for file_path in get_file_paths(proj_base_path): script = jedi.Script(path=file_path, project=jedi.Project(proj_base_path)) names = script.search(target_fn_name) for name in names: # 匹配目标函数的模块和类型 if name.module_name == target_module and name.type == 'method': target_def = name break if target_def: break if not target_def: print(f"未找到目标函数 {target_module}.{target_fn_name}") exit() # 第二步:构建并打印所有调用链 chains = build_call_chain(target_def) print(f"找到 {len(chains)} 条调用链:") for idx, chain in enumerate(chains, 1): print(f"调用链 {idx}:") print_call_chain(chain)
关键说明
get_full_name:将Jedi的Name对象转换为易读的完整限定名,方便输出展示。find_callers:通过references()获取函数的所有调用位置,再用parent()定位到调用所在的上层函数,递归遍历实现深层调用查找。build_call_chain:整理调用关系,从直接调用者向上追溯到顶层函数,构建完整的调用路径。- 防循环处理:用
visited集合记录已处理的节点,避免递归陷入死循环。
注意事项
- Jedi的静态分析存在局限性,无法识别动态生成的调用(如
getattr调用、动态拼接的函数名)。 - 必须明确目标函数的模块和类型,避免同名函数干扰查找结果。
- 大型项目中递归遍历可能较慢,可通过限制扫描目录(如只扫描
controllers、services)优化性能。
内容的提问来源于stack exchange,提问作者Moraes
相关产品推荐
相关产品推荐

