Python 代码:有关循环/条件的执行跟踪的信息

204*_*204 3 python unit-testing execution python-3.x

我想根据完成时执行的循环和条件来获取 python 函数的执行跟踪。但是,我想在不使用附加参数检测原始 python 函数的情况下执行此操作。例如:

def foo(a: int, b: int):
    while a:
        a = do_something()
        if b:
            a = do_something()


if __name__ == "__main__":
    foo(a, b)
Run Code Online (Sandbox Code Playgroud)

在执行之后,foo()我想要一个类似的执行跟踪: [while: true, if:false, while: true, if: true, while: false, ...]它记录了代码中条件评估的序列。有没有办法为任意的python函数自动获取这些信息?

我了解“覆盖率”python 模块返回“分支覆盖率”信息。但我不确定如何在这种情况下使用它?

MrP*_*rik 5

您可以将trace_conditions.py用作起点,并在需要时对其进行修改。

例子

foo 问题中定义的函数用于以下示例:

from trace_conditions import trace_conditions

# (1) This will just print conditions
traced_foo = trace_conditions(foo)
traced_foo(a, b)
# while c -> True
# if d -> True
# ...

# (2) This will return conditions
traced_foo = trace_conditions(foo, return_conditions=True)
result, conditions = traced_foo(a, b)
# conditions = [('while', 'c', True), ('if', 'd', True), ...)]
Run Code Online (Sandbox Code Playgroud)

注意ast.unparse用于获取条件的字符串表示。它是在 Python 3.9 中引入的。如果你想使用Python的旧版本,或许你会想安装第三方软件包astunparse,然后在函数中使用它_condition_to_string。否则trace_conditions不会返回条件的字符串表示。

TL; 博士

主意

基本上,我们希望以编程方式将捕获器添加到函数的代码中。例如,print接球手可能如下所示:

while x > 5:
    print('while x > 5', x > 5)  # <-- print condition after while
    # do smth

print('if x > 5', x > 5)  # <-- print condition before if
if x > 5:
    # do smth
Run Code Online (Sandbox Code Playgroud)

因此,主要思想是在 python 中使用代码自省工具(inspectastexec)。

执行

这里我简单解释一下trace_conditions.py 中的代码:

主功能 trace_conditions

主要功能不言自明,简单地反映了整个算法:(1)构建句法树;(2) 注入条件捕获器;(3) 编译新函数。

def trace_conditions(
        func: Callable, return_conditions=False):
    catcher_type = 'yield' if return_conditions else 'print'

    tree = _build_syntactic_tree(func)
    _inject_catchers(tree, catcher_type)
    func = _compile_function(tree, globals_=inspect.stack()[1][0].f_globals)

    if return_conditions:
        func = _gather_conditions(func)
    return func
Run Code Online (Sandbox Code Playgroud)

唯一需要解释的是globals_=inspect.stack()[1][0].f_globals。为了编译我们需要给蟒蛇由该函数使用的所有模块的新功能(例如,它可以使用mathnumpydjango等...)。并inspect.stack()[1][0].f_globals简单地获取在调用函数的模块中导入的所有内容。

警告!

# math_pi.py
import math

def get_pi():
   return math.pi


# test.py
from math_pi import get_pi
from trace_conditions import trace_conditions

traced = trace_conditions(get_pi)
traced()  # Error! Math is not imported in this module
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,你可以修改代码,trace_conditions.py或只加import mathtest.py

_build_syntactic_tree

在这里,我们首先使用得到的功能的源代码inspect.getsource,然后使用解析它在语法树ast.parse。不幸的是,如果从 调用,python 无法检查函数的源代码decorator,因此使用这种方法似乎无法使用方便的装饰器。

_inject_catchers

在这个函数中,我们遍历给定的语法树,找到whileif语句,然后在它们之前或之后注入捕获器。ast模块有方法walk,但它只返回节点本身(没有父节点),所以我实现了稍微改变的版本,walk它也返回父节点。如果我们想在之前插入 catcher,我们需要知道 parent if

def _inject_catchers(tree, catcher_type):
    for parent, node in _walk_with_parent(tree):
        if isinstance(node, ast.While):
            _catch_after_while(node, _create_catcher(node, catcher_type))
        elif isinstance(node, ast.If):
            _catch_before_if(parent, node, _create_catcher(node, catcher_type))
    ast.fix_missing_locations(tree)
Run Code Online (Sandbox Code Playgroud)

最后,我们调用ast.fix_missing_locations有助于正确填写技术领域的函数,例如lineno编译代码所需的其他领域。通常,在修改句法树时需要使用它。

捕获elif语句

有趣的是,python 在它的 ast 语法elif语句中没有,所以它只有if-else语句。该ast.If节点具有body包含if主体表达式的字段orelse和包含else块表达式的字段。并且elifcase 仅由字段ast.If内的节点表示orelse。这个事实反映在函数_catch_before_if 中

捕手(和_gather_conditions

有几种方法可以捕获条件,最简单的方法就是使用print它,但是如果您想稍后在 Python 代码中处理它们,这种方法将不起作用。一种直接的方法是拥有一个全局空列表,您将在执行函数期间在其中附加条件及其值。但是,我认为此解决方案在命名空间中引入了一个新名称,该名称可能会与函数内的本地名称混淆,因此我决定它应该对yield条件及其信息更安全。

该函数_gather_conditions正在为带有注入yield语句的函数添加一个包装器,它只是收集所有产生的条件并返回函数和条件的结果。