【问题标题】:get the lists of functions used/called within a function in python获取python函数中使用/调用的函数列表
【发布时间】:2018-08-17 19:19:54
【问题描述】:

是否有任何工具/库可以列出在另一个方法/函数中调用的方法/函数列表?

例如: 如果该工具或库针对以下方法运行

def calculate(a: int, b: int, operator: Operator):
    if operator == Operator.add:
        add(a, b)
    elif operator == Operator.subtract
        subtract(a, b)

那么它应该返回

1. add
2. subtract

这个问题与this one 几乎相同,但它是针对Java 的。

这与PyCharmFind Usage 所做的基本相同。 谢谢!

【问题讨论】:

  • 加减是函数调用而不是函数定义@Stack
  • @Stack 仍然回答错误的问题
  • @Stack Nope 这回答了谁调用 add 而不是 OP 的问题。所以没有。
  • 试试这个lib 来检索解释器堆栈。
  • 可以使用dis反汇编函数。查找 CALL_FUNCTION 字节码。您必须从它们向后工作以获取它们正在调用的函数(不会总是存在)。

标签: python python-3.x


【解决方案1】:

这似乎起作用了:

import dis
def list_func_calls(fn):
    funcs = []
    bytecode = dis.Bytecode(fn)
    instrs = list(reversed([instr for instr in bytecode]))
    for (ix, instr) in enumerate(instrs):
        if instr.opname=="CALL_FUNCTION":
            load_func_instr = instrs[ix + instr.arg + 1]
            funcs.append(load_func_instr.argval)

    return ["%d. %s" % (ix, funcname) for (ix, funcname) in enumerate(reversed(funcs), 1)]

例子:

>>> list_func_calls(calculate)
['1. add', '2. subtract']

这里发生的事情是:

  1. 我们为函数创建一个字节码对象
  2. 我们反转指令列表,因为函数名称将 跟随函数调用
  3. 我们单步执行列表,并且对于每个 CALL_FUNCTION 指令,

  4. 我们使用指令arg参数告诉我们有多少 我们得到的论点

  5. 我们往前看,找到加载函数的指令 我们打电话给

  6. 我们将该函数的名称 (instr.argval) 添加到列表中,然后 以请求的格式反转、枚举和返回

请注意,从 Python 3.6 开始,有三个 CALL_FUNCTION 指令,因此您必须查看文档以扩展此示例以在当前 python 中完全正常运行

【讨论】:

  • @FabianN 的答案是在我完成此操作时发布的,所以我想我会继续发布它。方法略有不同,很有趣。
  • 如果你有嵌套的函数调用,这会失败,用我的测试用例试试,但也许操作的用例仅限于简单的调用。
  • @FabianN。好点子。让我看看我是否可以改进一点。谢谢!
  • 我刚刚发现了下一个陷阱:函数调用中的列表解析,你可以用"." not in entry.argval过滤它们
  • 我的朋友,我很高兴你这么喜欢这段代码。感谢您鼓励我改进它!
【解决方案2】:

更新:增加了对Python2.7的兼容性
测试并确认与Python2.7Python3.5Python3.6 合作


指出dis 的功劳归Patrick Haugh¹
实现(dis 输出的解析)是我自己的:


设置:

import dis
import sys
from contextlib import contextmanager

# setup test environment
def a(_,__):
    pass

def b(_,__,___):
    pass

def c(_):
    pass

def g():
    pass 

d = 4

def test(flag):
    e = c

    if flag:
        a(a(b,c), [l for l in g(1, x=2)])
    else:
        b(a, int(flag), c(e))

    d = d + 1


def calculate(a, b, operator):
    if operator == Operator.add:
        add(a, b)
    elif operator == Operator.subtract:
        subtract(a, b)

class Operator(object):
    add = "add"
    subtract = "subtract"

Python 2/3 兼容性:

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

@contextmanager # https://stackoverflow.com/a/12111817/2422125
def captureStdOut(output):
    stdout = sys.stdout
    sys.stdout = output
    try:
        yield
    finally:
        sys.stdout = stdout


""" for Python <3.4 """
def get_instructions(func):
    import StringIO

    out = StringIO.StringIO()
    with captureStdOut(out):
        dis.dis(func)

    return [AttrDict({
               'opname': i[16:36].strip(),
               'arg': int(i[37:42].strip() or 0),
               'argval': i[44:-1].strip()
           }) for i in out.getvalue().split("\n")]


if sys.version_info < (3, 4):
    dis.get_instructions = get_instructions
    import __builtin__ as builtin
else:
    import builtins as builtin

代码:

def get_function_calls(func, built_ins=False):
    # the used instructions
    ins = list(dis.get_instructions(func))[::-1]

    # dict for function names (so they are unique)
    names = {}

    # go through call stack
    for i, inst in list(enumerate(ins))[::-1]:
        # find last CALL_FUNCTION
        if inst.opname[:13] == "CALL_FUNCTION":

            # function takes ins[i].arg number of arguments
            ep = i + inst.arg + (2 if inst.opname[13:16] == "_KW" else 1)

            # parse argument list (Python2)
            if inst.arg == 257:
                k = i+1
                while k < len(ins) and ins[k].opname != "BUILD_LIST":
                    k += 1

                ep = k-1

            # LOAD that loaded this function
            entry = ins[ep]

            # ignore list comprehensions / ...
            name = str(entry.argval)
            if "." not in name and entry.opname == "LOAD_GLOBAL" and (built_ins or not hasattr(builtin, name)):
                # save name of this function
                names[name] = True

            # reduce this CALL_FUNCTION and all its paramters to one entry
            ins = ins[:i] + [entry] + ins[ep + 1:]

    return sorted(list(names.keys()))

输出:

> print(get_function_calls(test))
> ['a', 'b', 'c', 'g']

> print(get_function_calls(test, built_ins=True))
> ['a', 'b', 'c', 'g', 'int']

> print(get_function_calls(calculate))
> ['add', 'subtract']

¹由于 Patrick Haughdis 的评论已超过 2 小时,我认为这是一个免费服用...

【讨论】:

  • 不错的方法,但目前会在带有 Python 3.6+ 的键盘参数的函数上中断。他们介绍了CALL_FUNCTION_KW(和CALL_FUNCTION_EX)。值得注意的是,它还可以捕获类实例化和 dict()tuple()int() 等内置函数...从技术上讲没有错,但可能不是用户可能想到的。
  • @Darkonaut 好点,我将添加一个开关来忽略类实例化等,解析CALL_FUNCTION_EX 会变得有趣......今天某个时候......我想
  • @Darkonaut 我得到了关键字参数,并添加了一个标志来禁用内置函数,但类构造函数现在必须保留(不知道如何在没有 eval 的情况下过滤它们)
  • @PaulRooney 感谢您指出这一点,但我唯一的意见是 Patrick Haugh 的评论“[...]你可以使用 dis 来反汇编函数 [...]”我没看到合适的。主要工作是解析输出,我只是在顶部添加了一个小注释作为基本礼貌。
  • 仍然需要一些改进。你没有在hasattr(builtin, name) 之前在这里定义builtin,所以当你设置built_ins=False 时你得到NameError 。如果您设置built_ins=True,它将不包含带有关键字参数的内置函数,例如enumerate([1,2], start=1).
猜你喜欢
  • 2019-07-01
  • 2021-04-05
  • 1970-01-01
  • 2017-05-31
  • 2017-04-22
  • 1970-01-01
  • 2015-05-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多