【问题标题】:"Online" monkey patching of a function“在线”猴子修补功能
【发布时间】:2014-11-05 18:01:46
【问题描述】:

您的程序刚刚在 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

这种方便的构造在遇到未知数据时会启动一个交互式调试器,因此我可以随意检查它并更改process_a_lot代码以正确处理它。

这里的问题是,当data_stream 很大时,您并不想再次咀嚼所有数据(假设next 很慢,因此您无法保存已有的数据并跳过下次运行)

当然,你可以在调试器中随意替换其他功能。你也可以替换函数本身,但它不会改变当前的执行上下文。

编辑: 由于有些人正在偏离轨道: 我知道有很多方法可以构建您的代码,以便您的处理功能与process_a_lot 分开。我并没有真正询问构建代码的方法,而是询问如何(在运行时)从代码未准备好处理替换的情况中恢复。

【问题讨论】:

  • 我不熟悉 pdb,但我想到的解决方案(除了提前弄清楚你在看什么)是有一个函数字典来处理每种输入。遇到新的种类,写一个新的函数,插入到dict中。
  • @user2357112 这是个好主意,是的。 如果你已经预料到了这个问题,有很多方法可以解决这个问题。但很多时候会出现你无法预料的问题(例如,数据结构不同,str.index 引发 ValueError)——你不能轻易纠正这些问题
  • 现在即时编译如此流行,我想即时编程是下一件大事.. grin.
  • @goncalopp 您可以有一个reload 调用 process_a_lot 函数中重新加载包含处理代码的模块。如果您遇到意外数据,您可以修改/创建适当的.py 文件并从重新加载再次播放。
  • @Assad 确实,如果您只想修改一个单独的函数,这很容易做到,正如我所提到的。但我想替换函数本身

标签: python function monkeypatching


【解决方案1】:

首先是(原型)解决方案,然后是一些重要的注意事项。

# 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)

还有:

# handlers.py

def handle_default(x):
    print "handle_default: {0!r}". format(x)

handler = {
    int: handle_default,
    str: handle_default
}

在 Python 2.7 中,这为您提供了一个字典,将预期/已知类型链接到处理每种类型的函数。如果某个类型没有可用的处理程序,则用户将自己放入调试器,让他们有机会使用适当的处理程序修改handlers.py 文件。在上面的示例中,floatcomplex 值没有处理程序。当它们出现时,用户将不得不添加适当的处理程序。例如,可以添加:

def handle_float(x):
    print "FIXED FLOAT {0!r}".format(x)

handler[float] = handle_float

然后:

def handle_complex(x):
    print "FIXED COMPLEX {0!r}".format(x)

handler[complex] = handle_complex

这是运行的样子:

$ 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)

好的,这样基本上就可以了。您可以将其改进并调整为更适合生产的形式,使其与 Python 2 和 3 等兼容。

在你这样做之前请仔细考虑。

这种“实时修改代码”的方法是一种极其脆弱的模式并且容易出错。它鼓励您在紧要关头进行实时热修复。这些修复可能没有很好或足够的测试。几乎按照定义,您此时才发现您正在处理一种新类型 T。您对 T、它为什么会发生、它的边缘情况和故障模式可能是什么等知之甚少。如果您的“修复”代码或热补丁不起作用,然后呢?当然,您可以进行更多的异常处理,捕获更多类的异常,并可能继续。

Flask 这样的Web 框架具有基本上以这种方式工作的调试模式。但这些是调试模式,通常不适合生产。此外,如果您在调试器中键入错误的命令怎么办?不小心输入“退出”而不是“继续”,整个程序就结束了,随之而来的是您保持处理活动的愿望。如果这是用于调试(可能是探索新类型的数据流),请使用。

如果这是用于生产用途,请考虑使用 strategy that sets aside unhandled-types 进行异步、带外检查和更正,而不是将开发人员/操作员置于实时处理流程的中间。

【讨论】:

  • +1 很好地探索了 user2357112 的想法,但它仍然没有回答我的主要问题(我不确定你是否注意到编辑)
【解决方案2】:

没有。

您不能为 当前 正在运行的 Python 函数打补丁并继续按下,就好像没有发生其他任何事情一样。至少不是以任何一般或实用的方式。

理论上,这是可能的——但仅限于在有限的情况下,需要付出很大的努力和魔法技能。不能一概而论。

要进行尝试,您必须:

  1. 找到相关函数源并进行编辑(直截了当)
  2. 将更改后的函数源编译为字节码(直截了当)
  3. 插入新字节码代替旧字节码(可行)
  4. 更改功能管家数据,使其指向程序中退出到 pdb 的“逻辑上”“同一点”。 (不确定,在某些情况下)
  5. 从调试器“继续”,返回到调试代码 (iffy)

如果您对函数内务处理和类似的调试器内务处理变量了解很多,在某些情况下您可能会实现 4 和 5。但请考虑:

  1. 调用 pdb 断点的字节码偏移量(框架对象中的f_lasti)可能会改变。你可能不得不缩小你的目标,“只改变函数源代码中的代码,而不是断点发生的地方”,以保持事情相当简单——否则,你必须能够计算 where em> 断点在新编译的字节码中。这可能是可行的,但同样受到限制(例如“只会调用pdb_trace() 一次,或类似的“为断点后分析留下面包屑”规定)。

  2. 你必须在修补 function, frame, and code objects 时保持敏锐。特别注意函数中的func_code__code__,如果你也支持 Python 3);框架中的f_lastif_linenof_code;以及代码中的co_codeco_lnotabco_stacksize

  3. 看在上帝的份上,希望您不打算更改函数的参数、名称或其他宏定义特征。这至少会使所需的内务量增加三倍。

  4. 更麻烦的是,添加新的局部变量(您想要更改程序行为的一个很常见的事情)非常非常不确定。它会影响f_localsco_nlocalsco_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
    
  5. 同样敏锐地修补一些跟踪调试位置的 pdb 值,因为当您键入“继续”时,这些值决定了控制流接下来的位置。

  6. 将可修补函数限制为具有相当静态状态的函数。例如,它们必须从不有可能在断点恢复之前被垃圾收集的对象,但在断点之后访问(例如在您的新代码中)。例如:

    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
    

    虽然“确保修补函数的执行环境与以往相同”对于许多值来说可能存在问题,但如果它们中的任何一个退出其正常的动态范围并在修补变更之前被垃圾收集,它肯定会崩溃和烧毁它们的动态范围/生命周期。

  7. 断言您只想在 CPython 上运行它,因为 PyPy、Jython 和其他 Python 实现甚至没有标准的 Python 字节码,它们的功能、代码和框架管理也不同。

我想说这种超级动态修补是可能的。而且我敢肯定,您可以通过大量的家务处理对象来构建它确实有效的简单案例。但是真正的代码有超出范围的对象。真正的补丁可能需要分配新的变量。等等。现实世界的条件大大增加了修补工作所需的工作量——并且在某些情况下,使修补完全不可能。

最后,你取得了什么成就?扩展数据流处理的一种非常脆弱、脆弱、不安全的方式。大多数猴子补丁都是在函数边界完成的,即使这样,也保留给一些非常高价值的用例,这是有原因的。生产数据流更好地服务adopting a strategy that sets aside unrecognized values for out-of-band examination and accommodation

【讨论】:

  • 关于任务复杂性的精彩文章! f_lasti 似乎也是只读的,因此您似乎实际上无法更改字节码上的位置
【解决方案3】:

如果我理解正确:

  • 你不想重复所有已经完成的工作

  • 一旦你弄清楚如何处理新数据,你需要一种方法来用新代码替换 #continue processing as usual

@user2357112 是在正确的轨道上:expected_types 应该是一个字典

data_type:(detect_function, handler_function)

detect_type 需要通过它来找到匹配项。如果没有找到匹配,pdb 会弹出,然后你可以弄清楚发生了什么,写一个新的detect_functionhandler_funcion,将它们添加到expected_typescontinue 从 pdb。

【讨论】:

  • 事实上,这是关于如何构造代码以允许替换函数的许多可能解决方案之一(还有一个只是简单地在每个不加选择的数据单元上调用一个单独的函数)。虽然所有这些都有帮助,但我提出的问题仍未得到解答 - 我已编辑问题以使其更清晰
【解决方案4】:

我想知道是否有办法对当前正在运行的函数 (process_a_lot) 进行修补,并“恢复”执行。

所以你想以某种方式从 pdb 中编写一个新的 process_a_lot 函数,然后在 pdb 调用的位置将控制权转移给它?

或者,您是否要在 pdb 外部重写函数,然后以某种方式从 .py 文件中重新加载该函数并将控制权转移到函数中间的 pdb 调用位置?

我能想到的唯一可能是:从pdb 中,导入你新编写的函数,然后用新函数中的字节码替换当前的process_a_lot 字节码(我认为是func.co_code或者其他的东西)。确保在 pdb 行之前未更改新函数中的任何内容(即使是 pdb 行),它可能会起作用。

但即使是这样,我想这是一个非常脆弱的解决方案。

【讨论】:

  • 你想的完全正确。这是一个非常脆弱的解决方案。
猜你喜欢
  • 2011-08-26
  • 2012-03-13
  • 2013-12-26
  • 2012-06-14
  • 2012-03-29
  • 2016-10-30
  • 2014-08-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多