如何提取Python代码文件中使用的函数?

13
我想创建一个代码文件中使用的所有函数列表。例如,如果我们有以下代码文件'add_random.py':

`

import numpy as np
from numpy import linalg

def foo():
    print np.random.rand(4) + np.random.randn(4)
    print linalg.norm(np.random.rand(4))
我想提取以下列表: [numpy.random.rand, np.random.randn, np.linalg.norm, np.random.rand] 该列表包含代码中使用的函数及其实际名称,格式为“模块.子模块.函数”。Python语言中是否有内置的功能可以帮助我完成这个任务?

5
这并不像你想象的那么简单。如果有作为引用存储在其他地方的可调用函数怎么办?比如说你有一个字典{'foo': np.random.rand, 'bar': linalg.norm},然后通过字典中的键使用这些可调用函数。需要考虑到你的代码可能会重新绑定这些键,动态地替换名称。 - Martijn Pieters
2
换句话说,解析完全限定名称并不一定是直截了当的。 - Martijn Pieters
1
话虽如此,您可以捕获所有的ast.Call节点并提取func表达式(它将是一个较小的ast节点树,包括ast.Nameast.Attribute)。 - Martijn Pieters
@MartijnPieters,您认为将代码解析为字符串并提取特定模式,然后进行多次处理直到获取所有使用的函数的完整路径可能是一个好主意吗? - Shishir Pandey
我认为多次遍历不会奏效;静态代码分析存在基本限制,动态代码太过动态。 - Martijn Pieters
1
你可以通过静态分析来完成这个任务,实际上你的示例并没有说明代码路径实际上使用了哪些函数,只是像你示例中写的那些。唯一的真正问题是我们可怕的工具和那些更看重玩世不恭而非沉默的学术后现代主义者。 - J. M. Becker
2个回答

10

您可以使用以下方法提取所有调用表达式:

import ast

class CallCollector(ast.NodeVisitor):
    def __init__(self):
        self.calls = []
        self.current = None

    def visit_Call(self, node):
        # new call, trace the function expression
        self.current = ''
        self.visit(node.func)
        self.calls.append(self.current)
        self.current = None

    def generic_visit(self, node):
        if self.current is not None:
            print "warning: {} node in function expression not supported".format(
                node.__class__.__name__)
        super(CallCollector, self).generic_visit(node)

    # record the func expression 
    def visit_Name(self, node):
        if self.current is None:
            return
        self.current += node.id

    def visit_Attribute(self, node):
        if self.current is None:
            self.generic_visit(node)
        self.visit(node.value)  
        self.current += '.' + node.attr

使用这个功能需要一个 ast 解析树:

tree = ast.parse(yoursource)
cc = CallCollector()
cc.visit(tree)
print cc.calls

示例:

>>> tree = ast.parse('''\
... def foo():
...     print np.random.rand(4) + np.random.randn(4)
...     print linalg.norm(np.random.rand(4))
... ''')
>>> cc = CallCollector()
>>> cc.visit(tree)
>>> cc.calls
['np.random.rand', 'np.random.randn', 'linalg.norm']

上面的遍历器只处理名称和属性;如果您需要更复杂的表达式支持,您将不得不扩展它。

请注意,像这样收集名称并非易事。任何间接引用都将无法处理。您可以在代码中构建一个函数字典来调用并动态交换函数对象,像上面的静态分析工具将无法跟踪它。


这对于简单函数非常有效。但是,当我们有函数的组合时,比如 np.random.rand(4).mean(),它就会失败。在这种情况下应该尝试做什么呢?理想情况下,我希望能够提取出np.random.randmean两个部分。 - Shishir Pandey
@ShishirPandey:你需要“知道”函数调用的返回类型,对于像NumPy这样的包,需要先收集信息;Python C扩展目前还没有自省功能(虽然Python 3.4 / 3.5正在努力解决这个问题)。也许你需要看看像CodeIntel这样的包,它可以让你为Python和其他语言生成代码自动完成功能(SublimeCodeIntel等使用了它),而不是重新发明轮子。 - Martijn Pieters

3
总的来说,这个问题是不可判定的,例如考虑getattribute(random, "random")()
如果你想进行静态分析,现在最好的选择是jedi
如果你接受动态解决方案,那么代码覆盖率就是你最好的朋友。它将展示所有使用的函数,而不仅仅是直接引用的函数。
最后,你也可以按照以下方式自己编写动态插装工具:
import random
import logging

class Proxy(object):
    def __getattr__(self, name):
        logging.debug("tried to use random.%s", name)
        return getattribute(_random, name)

_random = random
random = Proxy()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接