【问题标题】:How can I get the calling expression of a function in Python?如何在 Python 中获取函数的调用表达式?
【发布时间】:2015-03-30 11:05:05
【问题描述】:

出于教育目的,我希望能够打印当前函数的 complete 调用表达式。不一定来自异常处理程序。

经过一番研究,我最终得到了这段非常简单的代码:

import inspect
import linecache

def print_callexp(*args, **kwargs):
    try:
        frame = inspect.currentframe()

        # method 1, using inspect module only
        print(inspect.getframeinfo(frame.f_back).code_context)

        # method 2, just for the heck of it
        linecache.checkcache(frame.f_code.co_filename)
        line = linecache.getline(
            frame.f_back.f_code.co_filename,
            frame.f_back.f_lineno,
            frame.f_back.f_globals)
        print(line)

        # there's probably a similar method with traceback as well
    except:
        print("Omagad")

a_var = "but"
print_callexp(a_var, "why?!", 345, hello="world")

结果:

['    print_callexp(a_var, "why?!", 345, hello="world")\n']
    print_callexp(a_var, "why?!", 345, hello="world")

它完全符合我的要求,只要调用表达式位于一行上。但是多行表达式只会得到最后一行,显然需要我更深入地挖掘调用上下文。

# same example but with a multiple lines call
a_var = "but"
print_callexp(
    a_var, "why?!", 345, hello="world")

这给了我们:

['        a_var, "why?!", 345, hello="world")\n']
        a_var, "why?!", 345, hello="world")

如何正确打印完整的调用表达式?

“使用 lineno 值并应用一些 regex/eval 技巧”不是可接受的答案。我更喜欢更清洁的东西。我不介意导入更多模块,只要它们是 Python 3.x 标准库的一部分。但尽管如此,我会对任何参考资料感兴趣。

【问题讨论】:

  • 不确定python开发团队是否解决了这个问题。当我对代码中的异常进行故障排除时,它们通常只引用多行函数调用的最后一行。尝试的一种选择可能是遍历 lineno,并评估响应,直到没有抛出语法错误/异常。
  • @Jonathan Yep... 是这样想的,因此问题的最后一点:)
  • 这不是一个简单的问题要解决。您可以尝试将调用 file 解析为 AST tree,但表达式的哪一部分是调用者?例如,Python 允许广泛的动态调用约定。
  • 我会接受 Martijn 对 AST 的建议,只打印第 N 行的整个语句。你可能会得到一个完整的缩进块,但打印太多总比太少好。
  • IPython 只打印几行上下文; 2 之前和 2 之后,或类似。

标签: python debugging python-3.x introspection


【解决方案1】:

出于好奇,这是我用于这种非生产性目的的最终工作代码。乐趣无处不在! (几乎)

我不会立即将此标记为已接受的答案,希望有人能在不久的将来为我们提供更好的选择......

它按预期提取整个调用表达式。此代码假定调用表达式是一个裸函数调用,没有任何魔术、特殊技巧或嵌套/递归调用。这些特殊情况显然会使检测部分变得不那么琐碎,而且无论如何都超出了主题。

具体来说,我使用当前函数名帮助定位调用表达式的AST节点,以及inspect提供的行号作为起点。

我无法使用inspect.getsource() 来隔离调用者的块,这本来会更优化,因为我发现它返回不完整的源代码的情况。例如,当调用者的代码直接位于 ma​​in 的范围内时。不知道是bug还是功能...

获得源代码后,我们只需输入ast.parse() 即可获取根 AST 节点并遍历树以找到对当前函数的最新调用,瞧!

#!/usr/bin/env python3

import inspect
import ast

def print_callexp(*args, **kwargs):

    def _find_caller_node(root_node, func_name, last_lineno):
        # init search state
        found_node = None
        lineno = 0

        def _luke_astwalker(parent):
            nonlocal found_node
            nonlocal lineno
            for child in ast.iter_child_nodes(parent):
                # break if we passed the last line
                if hasattr(child, "lineno"):
                    lineno = child.lineno
                if lineno > last_lineno:
                    break

                # is it our candidate?
                if (isinstance(child, ast.Name)
                        and isinstance(parent, ast.Call)
                        and child.id == func_name):
                    # we have a candidate, but continue to walk the tree
                    # in case there's another one following. we can safely
                    # break here because the current node is a Name
                    found_node = parent
                    break

                # walk through children nodes, if any
                _luke_astwalker(child)

        # dig recursively to find caller's node
        _luke_astwalker(root_node)
        return found_node

    # get some info from 'inspect'
    frame = inspect.currentframe()
    backf = frame.f_back
    this_func_name = frame.f_code.co_name

    # get the source code of caller's module
    # note that we have to reload the entire module file since the
    # inspect.getsource() function doesn't work in some cases (i.e.: returned
    # source content was incomplete... Why?!).
    # --> is inspect.getsource broken???
    #     source = inspect.getsource(backf.f_code)
    #source = inspect.getsource(backf.f_code)
    with open(backf.f_code.co_filename, "r") as f:
        source = f.read()

    # get the ast node of caller's module
    # we don't need to use ast.increment_lineno() since we've loaded the whole
    # module
    ast_root = ast.parse(source, backf.f_code.co_filename)
    #ast.increment_lineno(ast_root, backf.f_code.co_firstlineno - 1)

    # find caller's ast node
    caller_node = _find_caller_node(ast_root, this_func_name, backf.f_lineno)

    # now, if caller's node has been found, we have the first line and the last
    # line of the caller's source
    if caller_node:
        #start_index = caller_node.lineno - backf.f_code.co_firstlineno
        #end_index = backf.f_lineno - backf.f_code.co_firstlineno + 1
        print("Hoooray! Found it!")
        start_index = caller_node.lineno - 1
        end_index = backf.f_lineno
        lineno = caller_node.lineno
        for ln in source.splitlines()[start_index:end_index]:
            print("  {:04d} {}".format(lineno, ln))
            lineno += 1

def main():
    a_var = "but"
    print_callexp(
        a_var, "why?!",
        345, (1, 2, 3), hello="world")

if __name__ == "__main__":
    main()

你应该得到这样的东西:

Hoooray! Found it!
  0079     print_callexp(
  0080         a_var, "why?!",
  0081         345, (1, 2, 3), hello="world")

感觉还是有点乱,但 OTOH,这是一个非常不寻常的目标。至少看起来在 Python 中足够不寻常。 例如,乍一看,我希望找到一种方法来直接访问已经加载的 AST 节点,inspect 可以通过框架对象或以类似的方式为其提供服务,而不必创建新的 AST手动节点。

请注意,我完全不知道这是否是特定于 CPython 的代码。它不应该是th'。至少从我从文档中读到的内容来看。

另外,我想知道ast 模块(或作为辅助模块)中为什么没有官方的 pretty-print 功能。 ast.dump() 可能会使用额外的 indent 参数来完成这项工作,以允许格式化输出并更轻松地调试 AST

顺便说一句,我发现这个非常简洁的function 有助于使用 AST。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-13
    • 1970-01-01
    • 2013-05-15
    • 2015-12-30
    • 2016-10-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多