Python代码:loops/conditionals的执行轨迹信息
Python Code: Information on Execution Trace of loops/conditionals
我想根据完成时执行的循环和条件获取 python 函数的执行轨迹。但是,我想在不使用附加参数检测原始 python 函数的情况下执行此操作。例如:
def foo(a: int, b: int):
while a:
a = do_something()
if b:
a = do_something()
if __name__ == "__main__":
foo(a, b)
执行 foo()
后,我想要一个类似这样的执行跟踪:
[while: true, if:false, while: true, if: true, while: false, ...]
它记录了代码中条件评估的顺序。有什么方法可以为任意 python 函数自动获取此信息?
我了解“覆盖”python 模块returns“分支覆盖”信息。但我不确定如何在这种情况下使用它?
您可以使用 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), ...)]
注意:ast.unparse
用于获取条件的字符串表示。它是在 Python 3.9 中引入的。如果您想使用旧版本 Python,也许您需要安装第 3 方包 astunparse and then use it in function _condition_to_string。否则 trace_conditions
将不会 return 条件的字符串表示。
TL;DR
想法
基本上,我们想以编程方式将捕捉器添加到函数的代码中。例如,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
所以,主要思路是在python(inspect, ast, exec)中使用代码自省工具。
实施
这里简单解释一下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
唯一需要解释的是globals_=inspect.stack()[1][0].f_globals
。为了编译一个新函数,我们需要给出 python 该函数使用的所有模块(例如,它可能使用 math
、numpy
、django
等...)。 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
要解决这个问题,您可以修改 trace_conditions.py
中的代码,或者在 test.py
中添加 import math
_build_syntactic_tree
这里我们首先使用inspect.getsource and then parse it in syntactic tree using ast.parse获取函数的源代码。不幸的是,python 无法检查从 decorator
调用的函数的源代码,因此使用这种方法似乎无法使用方便的装饰器。
_inject_catchers
在这个函数中,我们遍历给定的语法树,找到 while
和 if
语句,然后在它们之前或之后注入捕获器。 ast
模块有方法 walk,但它 return 只是节点本身(没有父节点),所以我实现了 walk
的略微更改版本,即 return 的父节点节点也是如此。如果我们想在 if
.
之前插入捕手,我们需要知道 parent
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)
最后我们调用 ast.fix_missing_locations
函数帮助正确填写 lineno
等技术领域和编译代码所需的其他领域。一般在修改句法树的时候需要用到。
捕获elif
语句
有趣的是 python 在它的 ast 语法中没有 elif
语句,所以它只有 if-else
语句。 ast.If
节点的字段 body
包含 if
主体的表达式,字段 orelse
包含 else
块的表达式。 elif
案例简单地由 orelse
字段内的 ast.If
节点表示。这个事实反映在函数 _catch_before_if.
中
捕手(和 _gather_conditions
)
有几种方法可以捕捉条件,最简单的就是 print
它,但是如果你想稍后在 python 代码中处理它们,这种方法将不起作用。一种直接的方法是拥有一个全局空列表,您将在函数执行期间在其中附加条件及其值。但是,我认为此解决方案在命名空间中引入了一个新名称,可能会与函数内的本地名称混淆,因此我决定它对 yield
条件及其信息应该更安全。
函数_gather_conditions
正在添加一个带有注入yield
语句的函数包装器,它简单地收集所有产生的条件和return函数和条件的结果。
我想根据完成时执行的循环和条件获取 python 函数的执行轨迹。但是,我想在不使用附加参数检测原始 python 函数的情况下执行此操作。例如:
def foo(a: int, b: int):
while a:
a = do_something()
if b:
a = do_something()
if __name__ == "__main__":
foo(a, b)
执行 foo()
后,我想要一个类似这样的执行跟踪:
[while: true, if:false, while: true, if: true, while: false, ...]
它记录了代码中条件评估的顺序。有什么方法可以为任意 python 函数自动获取此信息?
我了解“覆盖”python 模块returns“分支覆盖”信息。但我不确定如何在这种情况下使用它?
您可以使用 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), ...)]
注意:ast.unparse
用于获取条件的字符串表示。它是在 Python 3.9 中引入的。如果您想使用旧版本 Python,也许您需要安装第 3 方包 astunparse and then use it in function _condition_to_string。否则 trace_conditions
将不会 return 条件的字符串表示。
TL;DR
想法
基本上,我们想以编程方式将捕捉器添加到函数的代码中。例如,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
所以,主要思路是在python(inspect, ast, exec)中使用代码自省工具。
实施
这里简单解释一下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
唯一需要解释的是globals_=inspect.stack()[1][0].f_globals
。为了编译一个新函数,我们需要给出 python 该函数使用的所有模块(例如,它可能使用 math
、numpy
、django
等...)。 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
要解决这个问题,您可以修改 trace_conditions.py
中的代码,或者在 test.py
import math
_build_syntactic_tree
这里我们首先使用inspect.getsource and then parse it in syntactic tree using ast.parse获取函数的源代码。不幸的是,python 无法检查从 decorator
调用的函数的源代码,因此使用这种方法似乎无法使用方便的装饰器。
_inject_catchers
在这个函数中,我们遍历给定的语法树,找到 while
和 if
语句,然后在它们之前或之后注入捕获器。 ast
模块有方法 walk,但它 return 只是节点本身(没有父节点),所以我实现了 walk
的略微更改版本,即 return 的父节点节点也是如此。如果我们想在 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)
最后我们调用 ast.fix_missing_locations
函数帮助正确填写 lineno
等技术领域和编译代码所需的其他领域。一般在修改句法树的时候需要用到。
捕获elif
语句
有趣的是 python 在它的 ast 语法中没有 elif
语句,所以它只有 if-else
语句。 ast.If
节点的字段 body
包含 if
主体的表达式,字段 orelse
包含 else
块的表达式。 elif
案例简单地由 orelse
字段内的 ast.If
节点表示。这个事实反映在函数 _catch_before_if.
捕手(和 _gather_conditions
)
有几种方法可以捕捉条件,最简单的就是 print
它,但是如果你想稍后在 python 代码中处理它们,这种方法将不起作用。一种直接的方法是拥有一个全局空列表,您将在函数执行期间在其中附加条件及其值。但是,我认为此解决方案在命名空间中引入了一个新名称,可能会与函数内的本地名称混淆,因此我决定它对 yield
条件及其信息应该更安全。
函数_gather_conditions
正在添加一个带有注入yield
语句的函数包装器,它简单地收集所有产生的条件和return函数和条件的结果。