是否可以以编程方式构造 Python 堆栈帧并在代码中的任意点开始执行?

Posted

技术标签:

【中文标题】是否可以以编程方式构造 Python 堆栈帧并在代码中的任意点开始执行?【英文标题】:Is it possible to programmatically construct a Python stack frame and start execution at an arbitrary point in the code? 【发布时间】:2010-10-07 04:19:46 【问题描述】:

是否可以在 CPython 中以编程方式构造一个堆栈(一个或多个堆栈帧)并在任意代码点开始执行?想象以下场景:

    您有一个工作流引擎,其中可以使用 Python 编写工作流脚本,其中包含调用工作流引擎的一些结构(例如分支、等待/加入)。

    阻塞调用(例如等待或加入)在具有某种持久后备存储的事件调度引擎中设置侦听器条件。

    您有一个工作流脚本,它调用引擎中的等待条件,等待稍后将发出信号的某些条件。这会在事件调度引擎中设置监听器。

    工作流脚本的状态、包括程序计数器(或等效状态)在内的相关堆栈帧被保留 - 因为等待条件可能会在数天或数月后发生。

    在此期间,工作流引擎可能会停止并重新启动,这意味着必须能够以编程方式存储和重建工作流脚本的上下文。

    事件调度引擎触发等待条件启动的事件。

    工作流引擎读取序列化状态和堆栈,并使用堆栈重建线程。然后它在调用等待服务的位置继续执行。

问题

这可以用未经修改的 Python 解释器来完成吗?更好的是,谁能指出一些可能涵盖此类事情的文档或以编程方式构造堆栈帧并在代码块中间某处开始执行的代码示例?

编辑:为了澄清“未修改的 python 解释器”,我不介意使用 C API(PyThreadState 中是否有足够的信息来执行此操作?)但我不想去探索 Python 解释器的内部结构并不得不构建一个修改过的解释器。

更新:通过一些初步调查,可以通过PyThreadState_Get() 获取执行上下文。这将返回PyThreadState(在pystate.h 中定义)中的线程状态,它引用了frame 中的堆栈帧。堆栈帧保存在一个结构类型定义为PyFrameObject 的结构中,该结构在frameobject.h 中定义。 PyFrameObject 有一个字段 f_lasti(bobince 的属性),它有一个程序计数器,表示为距代码块开头的偏移量。

最后一点是个好消息,因为这意味着只要您保留实际编译的代码块,您就应该能够根据需要为尽可能多的堆栈帧重建局部变量并重新启动代码。我想说这意味着理论上可以不必修改 python 解释器,尽管这意味着代码仍然可能会与特定版本的解释器紧密耦合。

剩下的三个问题是:

事务状态和 'saga' 回滚,这可能可以通过一种用于构建 O/R 映射器的元类黑客来完成。我确实构建过一次原型,所以我对如何实现这一点有一个大致的了解。

稳健地序列化事务状态和任意局部变量。这可以通过读取__locals__(可从堆栈帧中获得)并以编程方式构造对pickle 的调用来完成。但是,我不知道这里可能存在什么问题(如果有的话)。

工作流的版本控制和升级。这有点棘手,因为系统没有为工作流节点提供任何符号锚。我们只有锚 为了做到这一点,必须识别所有入口点的偏移量并将它们映射到新版本。手动操作可能可行,但我怀疑很难自动化。如果您想支持此功能,这可能是最大的障碍。

更新 2: PyCodeObject (code.h) 在 PyCodeObject.co_lnotab 中有一个地址列表 (f_lasti)-> 行号映射(如果此处有误,请纠正我)。这可能用于促进将工作流更新到新版本的迁移过程,因为冻结的指令指针可以映射到新脚本中的适当位置,根据行号完成。仍然很混乱,但更有希望。

更新 3: 我认为这个问题的答案可能是 Stackless Python. 您可以暂停任务并将它们序列化。我还没有弄清楚这是否也适用于堆栈。

【问题讨论】:

好问题 - 我肯定不想成为必须调试这个项目的人! 【参考方案1】:

普通 Python 发行版中包含的 expat python 绑定以编程方式构建堆栈帧。但请注意,它依赖于未记录的私有 API。

http://svn.python.org/view/python/trunk/Modules/pyexpat.c?rev=64048&view=auto

【讨论】:

谢谢。非常有助于了解该机制的工作原理。【参考方案2】:

你通常想要的是延续,我看到这已经是这个问题的标签了。

如果您有能力使用系统中的所有代码,您可能想尝试一下 这样做而不是处理解释器堆栈内部。我不确定这将如何容易地持久化。

http://www.ps.uni-sb.de/~duchier/python/continuations.html

在实践中,我会构建您的工作流引擎,以便您的脚本将操作对象提交给经理。经理可以在任何时候腌制这组动作并允许 它们被加载并再次开始执行(通过恢复提交动作)。

换句话说:创建自己的应用程序级堆栈。

【讨论】:

我喜欢“动作对象”(想想:“命令”模式)的地方在于,它还可以为工作流的回滚提供支持。虽然这是对工作流脚本的简单性和清晰性的权衡,但从这个角度来看,这是一个很好的方法。【参考方案3】:

Stackless python 可能是最好的……如果你不介意完全使用不同的 python 发行版。 stackless 可以在 python 中序列化 everything 以及它们的 tasklet。如果你想留在标准的python发行版中,那么我会使用dill,它可以序列化几乎 python中的任何东西。

>>> import dill
>>> 
>>> def foo(a):
...   def bar(x):
...     return a*x
...   return bar
... 
>>> class baz(object):
...   def __call__(self, a,x):
...     return foo(a)(x)
... 
>>> b = baz()
>>> b(3,2)
6
>>> c = baz.__call__
>>> c(b,3,2)
6
>>> g = dill.loads(dill.dumps(globals()))
>>> g
'dill': <module 'dill' from '/Library/Frameworks/Python.framework/Versions/7.2/lib/python2.7/site-packages/dill-0.2a.dev-py2.7.egg/dill/__init__.pyc'>, 'c': <unbound method baz.__call__>, 'b': <__main__.baz object at 0x4d61970>, 'g': ..., '__builtins__': <module '__builtin__' (built-in)>, 'baz': <class '__main__.baz'>, '_version': '2', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x4d39d30>, '__doc__': None

Dill 将它的类型注册到 pickle 注册表中,所以如果你有一些使用 pickle 的黑盒代码并且你不能真正编辑它,那么只需导入 dill 就可以神奇地使其工作而无需猴子补丁第 3 方代码。

这是dill 腌制整个解释器会话...

>>> # continuing from above
>>> dill.dump_session('foobar.pkl')
>>>
>>> ^D
dude@sakurai>$ python
Python 2.7.5 (default, Sep 30 2013, 20:15:49) 
[GCC 4.2.1 (Apple Inc. build 5566)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> dill.load_session('foobar.pkl')
>>> c(b,3,2)
6

dill 也有 some good tools,用于帮助您了解在代码失败时导致酸洗失败的原因。

您还询问了它用于保存解释器状态的位置?

IPython 可以使用dill 将解释器会话保存到文件中。 https://nbtest.herokuapp.com/github/ipython/ipython/blob/master/examples/parallel/Using%20Dill.ipynb

klepto 使用dill 来支持避免重新计算的内存、磁盘或数据库缓存。 https://github.com/uqfoundation/klepto/blob/master/tests/test_cache_info.py

mystic 使用dill 通过保存优化器正在进行的状态来保存大型优化作业的检查点。 https://github.com/uqfoundation/mystic/blob/master/tests/test_solver_state.py

还有几个其他包使用dill 来保存对象或会话的状态。

【讨论】:

【参考方案4】:

您可以通过抛出异常并在回溯中向后退一帧来获取现有堆栈帧。问题是没有办法在代码块的中间(frame.f_lasti)恢复执行。

“可恢复异常”是一个非常有趣的语言理念,尽管很难想出一种合理的方式来与 Python 现有的“try/finally”和“with”块进行交互。

目前,执行此操作的常规方法是使用线程在与其控制器不同的上下文中运行您的工作流。 (如果您不介意编译它们,也可以使用协程/greenlets)。

【讨论】:

【参考方案5】:

在标准 CPython 中,堆栈中 C 和 Python 数据的混合使这变得复杂。重建调用堆栈需要同时重建 C 堆栈。这确实把它放在了一个太难的篮子里,因为它可能将实现与特定版本的 CPython 紧密耦合。

Stackless Python 允许对 tasklet 进行pickle,这提供了开箱即用所需的大部分功能。

【讨论】:

【参考方案6】:

我有相同类型的问题要解决。我想知道原始海报决定做什么。

stackless 声称只要没有关联的“受阻”C 堆栈(受阻是我选择的措辞),它就可以腌制小任务。

我可能会使用 eventlet 并找出某种方法来酸洗“状态”,但我真的不想编写显式状态机..

【讨论】:

遗憾的是,OP 暂时搁置了这个问题,因为原始项目从未进行过:^p【参考方案7】:

用joblib怎么样?

我不太确定这是否是您想要的,但它似乎符合拥有可以保留哪些阶段的工作流的想法。 Joblib 的用例似乎是为了避免重新计算,我不确定这是您在这里尝试做的还是更复杂的事情?

【讨论】:

以上是关于是否可以以编程方式构造 Python 堆栈帧并在代码中的任意点开始执行?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在 Oracle SQL*plus 脚本中以编程方式构造替换变量的名称?

如何以编程方式更改堆栈面板中所有(n个)texblock的字体大小?

以编程方式在 MSVC 和 Clang 上设置堆栈指针

以编程方式填充 UIStackView

GCC/GProf - 以编程方式访问线程的当前函数/堆栈跟踪

如何以编程方式在 Xcode 中快速设置 UIPageViewController 的过渡样式?