gon*_*opp 8 python monkeypatching function
你的程序暂停了pdb.set_trace().
有没有办法修补当前正在运行的功能,并"恢复"执行?
这可能通过调用框架操作吗?
一些背景:
通常情况下,我会有一个处理大量数据的复杂函数,而不具备我将找到的数据类型的先验知识:
def process_a_lot(data_stream):
#process a lot of stuff
#...
data_unit= data_stream.next()
if not can_process(data_unit)
import pdb; pdb.set_trace()
#continue processing
Run Code Online (Sandbox Code Playgroud)
这种方便的构造在遇到未知数据时启动交互式调试器,因此我可以随意检查它并更改process_a_lot代码以正确处理它.
这里的问题是,当data_stream你很大的时候,你真的不想再次咀嚼所有的数据(我们假设next它很慢,所以你不能保存已有的数据并在下一次运行时跳过)
当然,您可以在调试器中随意替换其他函数.您也可以替换函数本身,但不会更改当前的执行上下文.
编辑:
既然有些人越来越扯到:我知道有很多的结构化代码的方式,使得您的处理功能是分开的process_a_lot.我并没有真正询问如何构建代码的方法,以及如何从代码未准备好处理替换的情况中恢复(在运行时).
首先是(原型)解决方案,然后是一些重要的警告.
# process.py
import sys
import pdb
import handlers
def process_unit(data_unit):
global handlers
while True:
try:
data_type = type(data_unit)
handler = handlers.handler[data_type]
handler(data_unit)
return
except KeyError:
print "UNUSUAL DATA: {0!r}". format(data_unit)
print "\n--- INVOKING DEBUGGER ---\n"
pdb.set_trace()
print
print "--- RETURNING FROM DEBUGGER ---\n"
del sys.modules['handlers']
import handlers
print "retrying"
process_unit("this")
process_unit(100)
process_unit(1.04)
process_unit(200)
process_unit(1.05)
process_unit(300)
process_unit(4+3j)
sys.exit(0)
Run Code Online (Sandbox Code Playgroud)
和:
# handlers.py
def handle_default(x):
print "handle_default: {0!r}". format(x)
handler = {
int: handle_default,
str: handle_default
}
Run Code Online (Sandbox Code Playgroud)
在Python 2.7中,它为您提供了一个字典,将预期/已知类型链接到处理每种类型的函数.如果某个类型没有可用的处理程序,则将用户放入调试器中,使其有机会handlers.py使用适当的处理程序修改该文件.在上面的示例中,没有处理程序float或complex值.当他们来时,用户将不得不添加适当的处理程序.例如,有人可能会添加:
def handle_float(x):
print "FIXED FLOAT {0!r}".format(x)
handler[float] = handle_float
Run Code Online (Sandbox Code Playgroud)
然后:
def handle_complex(x):
print "FIXED COMPLEX {0!r}".format(x)
handler[complex] = handle_complex
Run Code Online (Sandbox Code Playgroud)
这是运行的样子:
$ python process.py
handle_default: 'this'
handle_default: 100
UNUSUAL DATA: 1.04
--- INVOKING DEBUGGER ---
> /Users/jeunice/pytest/testing/sfix/process.py(18)process_unit()
-> print
(Pdb) continue
--- RETURNING FROM DEBUGGER ---
retrying
FIXED FLOAT 1.04
handle_default: 200
FIXED FLOAT 1.05
handle_default: 300
UNUSUAL DATA: (4+3j)
--- INVOKING DEBUGGER ---
> /Users/jeunice/pytest/testing/sfix/process.py(18)process_unit()
-> print
(Pdb) continue
--- RETURNING FROM DEBUGGER ---
retrying
FIXED COMPLEX (4+3j)
Run Code Online (Sandbox Code Playgroud)
好的,这基本上有效.您可以将其改进并调整为更适合生产的形式,使其在Python 2和3中兼容,等等.
在你这样做之前,请仔细思考.
这种"实时修改代码"方法是一种令人难以置信的脆弱模式和容易出错的方法.它鼓励您在不久的将来进行实时热修复.这些修复可能没有充分或充分的测试.几乎按照定义,你刚才发现你正在处理一个新的类型T.你还不太了解T,它为什么会发生,它的边缘情况和失败模式可能是什么等等.如果你的"修复"代码或热补丁不起作用,然后呢?当然,您可以进行更多的异常处理,捕获更多类别的异常,并可能继续.
像Flask这样的Web框架具有基本上以这种方式工作的调试模式.但这些是调试模式,通常不适合生产.而且,如果在调试器中键入错误的命令怎么办?意外地键入"退出"而不是"继续",整个程序结束,并且随之而来,您希望保持处理活着.如果这是用于调试(可能探索新类型的数据流),请使用.
如果这是用于生产用途,请考虑采用一种策略来为异步,带外检查和纠正留出未处理类型,而不是将开发人员/操作员放在实时处理流程中间的策略.
不。
你不能用金钱修补当前正在运行的 Python 函数并继续按下,就好像什么也没发生一样。至少不是以任何一般或实际的方式。
从理论上讲,这是可能的——但只有在有限的情况下,需要付出很大的努力和神奇的技巧。这不能一概而论。
要进行尝试,您必须:
在某些情况下,如果您对函数内务管理和类似的调试器内务管理变量了解很多,您可能会实现 4 和 5。但请考虑:
调用 pdb 断点的字节码偏移量(f_lasti在框架对象中)可能会改变。您可能必须缩小目标,“仅更改函数源代码中比断点发生更靠后的代码”,以使事情变得相当简单——否则,您必须能够计算出断点在函数中的位置。新编译的字节码。这可能是可行的,但同样受到限制(例如“只会调用pdb_trace()一次,或类似的“为断点后分析留下面包屑”规定)。
您必须善于修补函数、框架和代码对象。特别注意func_code函数中的(__code__如果您也支持 Python 3);框内的f_lasti、f_lineno、 和;f_code和co_code、co_lnotab、 和co_stacksize在代码中。
看在上帝的份上,希望您不打算更改函数的参数、名称或其他宏定义特征。这至少会使所需的家政工作量增加三倍。
更麻烦的是,添加新的局部变量(你想要改变程序行为的一个非常常见的事情)是非常非常不确定的。它会影响f_locals、,co_nlocals并且co_stacksize很可能会完全重新排列字节码访问值的顺序和方式。您可以通过像x = None所有原始本地变量一样添加赋值语句来最大程度地减少这种情况。但根据字节码的变化方式,您甚至可能需要对 Python 堆栈进行热修补,而这在 Python 本身中是无法完成的。因此那里可能需要 C/Cython 扩展。
这是一个非常简单的示例,表明即使对非常简单的函数进行微小的更改,字节码排序和参数也可能会发生显着变化:
def a(x): LOAD_FAST 0 (x)
y = x + 1 LOAD_CONST 1 (1)
return y BINARY_ADD
STORE_FAST 1 (y)
LOAD_FAST 1 (y)
RETURN_VALUE
------------------ ------------------
def a2(x): LOAD_CONST 1 (2)
inc = 2 STORE_FAST 1 (inc)
y = x + inc LOAD_FAST 0 (x)
return y LOAD_FAST 1 (inc)
BINARY_ADD
STORE_FAST 2 (y)
LOAD_FAST 2 (y)
RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)同样敏锐地修补一些跟踪调试位置的 pdb 值,因为当您键入“继续”时,这些值决定了控制流下一步的走向。
将可修补的函数限制为那些具有相当静态状态的函数。例如,它们绝不能具有在断点恢复之前可能被垃圾收集但在断点之后访问的对象(例如在新代码中)。例如:
some = SomeObject()
# blah blah including last touch of `some`
# ...
pdb.set_trace()
# Look, Ma! I'm monkey-patching!
if some.some_property:
# oops, `some` was GC'd - DIE DIE DIE
Run Code Online (Sandbox Code Playgroud)
虽然“确保修补函数的执行环境与以前相同”对于许多值来说可能存在问题,但如果它们中的任何一个退出其正常动态范围并在修补改变其动态范围之前被垃圾收集,那么它肯定会崩溃并烧毁/寿命。
断言您只想在 CPython 上运行它,因为 PyPy、Jython 和其他 Python 实现甚至没有标准的 Python 字节码,并且以不同的方式执行其函数、代码和框架管理。
我想说这种超动态修补是可能的。我确信您可以通过大量的内务对象调整来构建它确实有效的简单案例。但实际代码中的对象超出了范围。真正的补丁可能需要分配新的变量。等等。现实世界的条件极大地增加了修补工作所需的工作量——并且在某些情况下,使得修补完全不可能。
归根结底,你取得了什么成就?这是一种非常脆弱、脆弱且不安全的扩展数据流处理的方式。大多数猴子修补都是在函数边界完成的,即使这样,也保留给一些非常高价值的用例,这是有原因的。采用为带外检查和住宿留出未识别值的策略可以更好地服务于生产数据流。