"在线"猴子修补功能

时间:2014-11-05 18:01:46

标签: python function monkeypatching

您的程序暂停了pdb.set_trace()

有没有办法让补丁当前正在运行的功能," resume"执行吗

这可以通过调用框架操作吗?


某些背景信息:

通常情况下,我会有一个处理大量数据的复杂函数,而不会先验知道我会发现什么类型的数据:

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 分开。我并没有真正询问如何构建代码的方法,以及如何从代码未准备好处理替换的情况中恢复(在运行时)。

4 个答案:

答案 0 :(得分:5)

首先是(原型)解决方案,然后是一些重要的警告。

# 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 :(得分:4)

您无法修补当前运行的Python函数,并继续按下,就好像没有其他任何事情发生过一样。至少不是以任何一般或实际的方式。

从理论上讲,它是可能的 - 但只有在有限的情况下,才能付出很大的努力和精湛的技巧。任何一般性都无法做到这一点。

要进行尝试,您必须:

  1. 找到相关的功能源并进行编辑(直截了当)
  2. 将更改的函数源编译为字节码(直接)
  3. 插入新的字节码代替旧的(可行的)
  4. 将功能管家数据改为指向"逻辑" "同一点"在它退出到pdb的程序中。 (iffy,在某些情况下)
  5. "继续"从调试器,回到调试代码(iffy)
  6. 在某些情况下,如果您对功能管理和类似调试器管理变量有很多了解,那么您可能会达到4和5。但请考虑:

    1. 调用pdb断点(帧对象中为f_lasti)的字节码偏移量可能会发生变化。你可能不得不缩小你的目标,只改变函数源代码中的代码而不是断点发生的#34;保持合理简单 - 否则,您必须能够计算断点在新编译的字节码中的位置。这可能是可行的,但是再次受到限制(例如"只会调用pdb_trace()一次,或类似的"留下面包屑用于断点后分析"规定)。

      < / LI>
    2. 你需要修补function, frame, and code objects。如果您还支持Python 3,请特别注意函数中的func_code __code__;框架中有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值时同样敏锐,因为当您键入&#34; continue,&#34;这些决定了控制流程的下一步。

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

      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
      

      虽然&#34;确保修补功能的执行环境与以前相同&#34;对于许多值而言可能存在问题,如果它们中的任何一个退出其正常的动态范围并且在修补之前进行垃圾收集,则会保证崩溃和刻录,从而改变其动态范围/生命周期。

    7. 断言你只想在CPython上运行它,因为PyPy,Jython和其他Python实现甚至没有标准的Python字节码,并且不同地执行它们的功能,代码和帧内务。

    8. 我很想说这种超级动态修补是可能的。而且我确信你可以通过大量的管家对象来构建简单的工具。但实际代码的对象超出了范围。真正的补丁可能需要分配新变量。等等。真实的世界条件大大增加了修补工作所需的工作量 - 在某些情况下,使修补工作完全不可能。

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

答案 2 :(得分:2)

如果我理解正确:

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

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

@ user2357112走在正确的轨道上:expected_types应该是

的字典
data_type:(detect_function, handler_function)

detect_type需要通过它来查找匹配项。如果未找到匹配项,则会弹出pdb,然后您可以找出正在发生的情况,撰写新的detect_functionhandler_funcion,将其添加到expected_types和{{ 1}}来自pdb。

答案 3 :(得分:2)

  

我想知道的是,是否有办法修补当前正在运行的功能(process_a_lot)和“恢复”执行。

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

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

我能想到的唯一可能性是:从pdb内导入新编写的函数,然后用新函数中的字节代码替换当前的process_a_lot字节代码(我认为它是func.co_code或其他东西)。确保在pdb行之前的新功能(甚至不是pdb行)中没有任何更改,并且可能工作。

但即便如此,我认为这是一个非常脆弱的解决方案。