在帧的原始本地作用域中执行代码
我写了一个远程的Python调试器,其中一个功能是能够在程序暂停在断点时执行任意代码。我的调试器使用以下方法来执行从远程调试器接收到的代码:
exec (compile(code, '<string>', 'single') , frame.f_globals, frame.f_locals)
这个方法大部分情况下都能正常工作,但我发现了几个问题。
赋值语句并没有真正应用到原来的局部变量字典中。这可能是因为f_locals是只读的。
如果在类的方法中暂停,访问受保护的属性(名字以双下划线开头)就不行。我猜这是因为Python对受保护属性进行了名称改编。
所以我想问,有没有办法解决这些限制?我能否让Python误以为代码是在那个框架的实际局部作用域中执行的?
我使用的是CPython 2.7,并且愿意接受针对这个版本的解决方案或黑科技。
3 个回答
在调试时,如果程序停在一个断点上,我能否让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
函数中使用compile
和exec
来实现这一点,这相当于:
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
的作用范围将不是用户程序,而是定义该函数的上下文,也就是调试器!这方面也有解决办法,但由于没有特别提到,就留给读者自己去探索吧,暂时就这样。^__^
我不太确定我是否理解你的意思,但 exec
确实会把代码中的赋值结果放到 locals
参数里:
>>> loc = {}
>>> exec(compile('a=3', '<string>', 'single'), {}, loc)
>>> loc
{'a': 3}
也许 f_locals
不允许写入。
赋值语句其实并不是直接作用于原来的局部变量字典。这可能是因为 f_locals 是只读的。
并不是完全这样,但函数的字节码不会查看 locals
,而是使用一种简单但重要的优化方法,把局部变量放在一个简单的数组里,这样就避免了运行时查找。要想避免这种优化(并让函数变得非常、非常慢),就得编译不同的代码,比如以 exec ''
开头的代码,这样可以强制编译器不使用优化(在 Python 2 中可以;在 Python 3 中没有办法)。如果你需要处理现有的字节码,那就没办法了:你无法实现你想要的效果。
如果在一个类的方法中停止,访问受保护的属性(名字以双下划线开头)是行不通的。我猜这是因为 Python 对受保护属性进行了名称改写。
没错,所以这个问题确实有解决办法:在名字前加上 _Classname
,这样可以模拟编译器的做法。需要注意的是,双下划线前缀表示私有属性;而受保护属性只需要一个下划线(这样就不会有问题)。私有属性主要是为了避免在子类中意外出现同名的类属性(在这个目的上效果还不错,虽然不是完美的,也不适用于其他情况;-)。