在帧的原始本地作用域中执行代码

2 投票
3 回答
887 浏览
提问于 2025-04-16 02:19

我写了一个远程的Python调试器,其中一个功能是能够在程序暂停在断点时执行任意代码。我的调试器使用以下方法来执行从远程调试器接收到的代码:

exec (compile(code, '<string>', 'single') , frame.f_globals, frame.f_locals)

这个方法大部分情况下都能正常工作,但我发现了几个问题。

  1. 赋值语句并没有真正应用到原来的局部变量字典中。这可能是因为f_locals是只读的。

  2. 如果在类的方法中暂停,访问受保护的属性(名字以双下划线开头)就不行。我猜这是因为Python对受保护属性进行了名称改编。

所以我想问,有没有办法解决这些限制?我能否让Python误以为代码是在那个框架的实际局部作用域中执行的?

我使用的是CPython 2.7,并且愿意接受针对这个版本的解决方案或黑科技。

3 个回答

0

在调试时,如果程序停在一个断点上,我能否让Python误以为代码正在该位置的实际局部环境中执行呢?

Python的调试工具pdb可以做到这一点。比如说,你正在调试一个文件tests/scopeTest.py,而你的程序中有一行代码,其中的变量在程序里并没有被声明:

print (NOT_DEFINED_IN_PROGRAM)

这样,当你运行代码python tests/scopeTest.py时,会出现:

NameError: name 'NOT_DEFINED_IN_PROGRAM' is not defined

现在,你希望在调试器停在那一行时定义这个变量,并让程序继续执行,使用这个变量,就好像它一直在程序中被定义一样。换句话说,你想在那个范围内进行更改,这样你就可以继续执行,并且这个更改是永久的。实际上,这是可能的:

$ python -m pdb tests/scopeTest.py
> /home/user/tests/scopeTest.py(1)<module>()
-> print (NOT_DEFINED_IN_PROGRAM)
(Pdb) 'NOT_DEFINED_IN_PROGRAM' in locals()
False
(Pdb) NOT_DEFINED_IN_PROGRAM = 5
(Pdb) 'NOT_DEFINED_IN_PROGRAM' in locals()
True
(Pdb) step
5

pdb通过在其default函数中使用compileexec来实现这一点,这相当于:

code = compile(line + '\n', <stdin>, 'single')
exec(code, self.curframe.f_globals, self.curframe_locals)

其中self.curframe是一个特定的帧。现在,self.curframe_locals并不是self.curframe.f_locals,因为正如setup函数所说:

# The f_locals dictionary is updated from the actual frame
# locals whenever the .f_locals accessor is called, so we
# cache it here to ensure that modifications are not overwritten.
self.curframe_locals = self.curframe.f_locals

希望这对你有帮助,也正是你想要的!

请注意,即使如此,如果你想在调试的程序上下文中用一个猴子补丁版本替换一个函数,比如:

newGlobals['abs'] = myCustomAbsFunction
exec(code, newGlobals, locals)

那么myCustomAbsFunction的作用范围将不是用户程序,而是定义该函数的上下文,也就是调试器!这方面也有解决办法,但由于没有特别提到,就留给读者自己去探索吧,暂时就这样。^__^

0

我不太确定我是否理解你的意思,但 exec 确实会把代码中的赋值结果放到 locals 参数里:

>>> loc = {}
>>> exec(compile('a=3', '<string>', 'single'), {}, loc)
>>> loc
{'a': 3}

也许 f_locals 不允许写入。

2

赋值语句其实并不是直接作用于原来的局部变量字典。这可能是因为 f_locals 是只读的。

并不是完全这样,但函数的字节码不会查看 locals,而是使用一种简单但重要的优化方法,把局部变量放在一个简单的数组里,这样就避免了运行时查找。要想避免这种优化(并让函数变得非常非常慢),就得编译不同的代码,比如以 exec '' 开头的代码,这样可以强制编译器不使用优化(在 Python 2 中可以;在 Python 3 中没有办法)。如果你需要处理现有的字节码,那就没办法了:你无法实现你想要的效果。

如果在一个类的方法中停止,访问受保护的属性(名字以双下划线开头)是行不通的。我猜这是因为 Python 对受保护属性进行了名称改写。

没错,所以这个问题确实有解决办法:在名字前加上 _Classname,这样可以模拟编译器的做法。需要注意的是,双下划线前缀表示私有属性;而受保护属性只需要一个下划线(这样就不会有问题)。私有属性主要是为了避免在子类中意外出现同名的类属性(在这个目的上效果还不错,虽然不是完美的,也不适用于其他情况;-)。

撰写回答