如何在 python 包中查找 python 函数或变量的所有用途

How to find all uses of a python function or variable in a python package

我试图在函数级别映射 python 包中的 uses/causes 函数和变量。有几个模块在其他函数中使用了 functions/variables,我想创建一个看起来像这样的字典:

{'function_name':{'uses': [...functions used in this function...],
                  'causes': [...functions that use this function...]},
 ...
}

我所指的功能需要在包的模块中定义。

我该如何开始呢?我知道我可以遍历包 __dict__ 并通过执行以下操作测试包中定义的函数:

import package

import inspect
import types

for name, obj in vars(package).items():
    if isinstance(obj, types.FunctionType):
        module, *_ = inspect.getmodule(obj).__name__.split('.')
        if module == package.__name__:
            # Now that function is obtained need to find usages or functions used within it

但之后我需要找到当前函数中使用的函数。如何才能做到这一点?是否已经为此类工作开发了一些东西?我认为分析库可能必须做类似的事情。

评论中建议的 ast 模块最终运行良好。这是我创建的一个class,用于提取每个函数中使用的包中定义的函数或变量。

import ast
import types
import inspect


class CausalBuilder(ast.NodeVisitor):

    def __init__(self, package):
        self.forest = []
        self.fnames = []

        for name, obj in vars(package).items():
            if isinstance(obj, types.ModuleType):
                with open(obj.__file__) as f:
                    text = f.read()
                tree = ast.parse(text)
                self.forest.append(tree)
            elif isinstance(obj, types.FunctionType):
                mod, *_ = inspect.getmodule(obj).__name__.split('.')
                if mod == package.__name__:
                    self.fnames.append(name)

        self.causes = {n: [] for n in self.fnames}

    def build(self):
        for tree in self.forest:
            self.visit(tree)
        return self.causes

    def visit_FunctionDef(self, node):
        self.generic_visit(node)
        for b in node.body:
            if node.name in self.fnames:
                self.causes[node.name] += self.extract_cause(b)

    def extract_cause(self, node):
        nodes = [node]
        cause = []
        while nodes:
            for i, n in enumerate(nodes):
                ntype = type(n)
                if ntype == ast.Name:
                    if n.id in self.fnames:
                        cause.append(n.id)
                elif ntype in (ast.Assign, ast.AugAssign, ast.Attribute,
                               ast.Subscript, ast.Return):
                    nodes.append(n.value)
                elif ntype in (ast.If, ast.IfExp):
                    nodes.append(n.test)
                    nodes.extend(n.body)
                    nodes.extend(n.orelse)
                elif ntype == ast.Compare:
                    nodes.append(n.left)
                    nodes.extend(n.comparators)
                elif ntype == ast.Call:
                    nodes.append(n.func)
                elif ntype == ast.BinOp:
                    nodes.append(n.left)
                    nodes.append(n.right)
                elif ntype == ast.UnaryOp:
                    nodes.append(n.operand)
                elif ntype == ast.BoolOp:
                    nodes.extend(n.values)
                elif ntype == ast.Num:
                    pass
                else:
                    raise TypeError("Node type `{}` not accounted for."
                                    .format(ntype))

                nodes.pop(nodes.index(n))

        return cause

可以通过首先导入 python 包并传递给构造函数来使用 class,然后像这样调用 build 方法:

import package

cb = CausalBuilder(package)
print(cb.build())

这将打印出一个字典,其中包含一组表示函数名称的键,以及表示函数和/或函数中使用的变量的列表的值。并非所有 ast 类型都被考虑在内,但这对我来说已经足够了。

该实现递归地将节点分解为更简单的类型,直到达到 ast.Name,之后它可以提取目标函数中正在使用的变量、函数或方法的名称。