如何检测 Python AST 以跟踪存储操作?
How to instrument Python AST for tracing store operations?
My debugger 使用 AST 工具获取代码执行的所有逻辑步骤(包括表达式计算中的步骤)的通知。
有一个步骤我无法确定 -- for 循环即将为循环变量分配新值的那一刻。
在 For
节点内,循环变量(或更复杂的东西)用 target
属性内的表达式表示。此表达式的 ctx
属性设置为 ast.Store()
。不知道怎么追查这个节点的使用情况
作为一个特例,我可以用 locals()
:
的索引替换简单的循环变量
for locals()["i"] in range(10):
print(i)
这会在 ctx=ast.Store()
节点内给我一个 ctx=ast.Load()
节点,我知道如何跟踪它。不幸的是,这不会扩展到更复杂的目标。
解释器如何使用这些ctx=ast.Store()
表达式?我能否以某种方式直接对它们进行检测,以便在解释器执行存储操作时得到通知?
一种选择是重写 for
循环,以便赋值以一个临时变量为目标,并将您的跟踪代码插入到循环体中。例如,这样的循环:
for foo.x in range(3):
print(foo.x)
可以重写为:
for _temp in range(3):
print('loop variable will be set to', _temp)
foo.x = _temp
print(foo.x)
为此,我们实施 NodeTransformer
:
class ForLoopRewriter(ast.NodeTransformer):
def __init__(self, nodes_to_insert):
super().__init__()
self.nodes_to_insert = nodes_to_insert
def visit_For(self, node):
# redirect the assignment to a usually invalid variable name so it
# doesn't clash with other variables in the code
target = ast.Name('@loop_var', ast.Store())
# insert the new nodes
loop_body = self.nodes_to_insert.copy()
# then reassign the loop variable to the actual target
reassign = ast.Assign([node.target], ast.Name('@loop_var', ast.Load()))
loop_body.append(reassign)
# visit all the ast nodes in the loop body
for n in node.body:
loop_body.append(self.visit(n))
# make a new For node and return it
new_node = ast.For(target, node.iter, loop_body, node.orelse)
ast.fix_missing_locations(new_node)
return new_node
可以这样使用:
code = '''
class Foo:
@property
def x(self):
pass
@x.setter
def x(self, x):
print('Setting x')
foo = Foo()
itr = (print('yielding', x) for x in range(1))
for foo.x in itr:
pass
'''
tree = ast.parse(code)
tracing_code = ast.parse('print("Your tracing code")').body
tree = ForLoopRewriter(tracing_code).visit(tree)
codeobj = compile(tree, 'foo.py', 'exec')
exec(codeobj)
# output:
# yielding 0
# Your tracing code
# Setting x
My debugger 使用 AST 工具获取代码执行的所有逻辑步骤(包括表达式计算中的步骤)的通知。
有一个步骤我无法确定 -- for 循环即将为循环变量分配新值的那一刻。
在 For
节点内,循环变量(或更复杂的东西)用 target
属性内的表达式表示。此表达式的 ctx
属性设置为 ast.Store()
。不知道怎么追查这个节点的使用情况
作为一个特例,我可以用 locals()
:
for locals()["i"] in range(10):
print(i)
这会在 ctx=ast.Store()
节点内给我一个 ctx=ast.Load()
节点,我知道如何跟踪它。不幸的是,这不会扩展到更复杂的目标。
解释器如何使用这些ctx=ast.Store()
表达式?我能否以某种方式直接对它们进行检测,以便在解释器执行存储操作时得到通知?
一种选择是重写 for
循环,以便赋值以一个临时变量为目标,并将您的跟踪代码插入到循环体中。例如,这样的循环:
for foo.x in range(3):
print(foo.x)
可以重写为:
for _temp in range(3):
print('loop variable will be set to', _temp)
foo.x = _temp
print(foo.x)
为此,我们实施 NodeTransformer
:
class ForLoopRewriter(ast.NodeTransformer):
def __init__(self, nodes_to_insert):
super().__init__()
self.nodes_to_insert = nodes_to_insert
def visit_For(self, node):
# redirect the assignment to a usually invalid variable name so it
# doesn't clash with other variables in the code
target = ast.Name('@loop_var', ast.Store())
# insert the new nodes
loop_body = self.nodes_to_insert.copy()
# then reassign the loop variable to the actual target
reassign = ast.Assign([node.target], ast.Name('@loop_var', ast.Load()))
loop_body.append(reassign)
# visit all the ast nodes in the loop body
for n in node.body:
loop_body.append(self.visit(n))
# make a new For node and return it
new_node = ast.For(target, node.iter, loop_body, node.orelse)
ast.fix_missing_locations(new_node)
return new_node
可以这样使用:
code = '''
class Foo:
@property
def x(self):
pass
@x.setter
def x(self, x):
print('Setting x')
foo = Foo()
itr = (print('yielding', x) for x in range(1))
for foo.x in itr:
pass
'''
tree = ast.parse(code)
tracing_code = ast.parse('print("Your tracing code")').body
tree = ForLoopRewriter(tracing_code).visit(tree)
codeobj = compile(tree, 'foo.py', 'exec')
exec(codeobj)
# output:
# yielding 0
# Your tracing code
# Setting x