问题 处理异常后如何从上次尝试的指令继续执行帧?


我想处理一个 NameError 异常通过将所需的缺失变量注入到帧中,然后从上一次尝试的指令继续执行。

以下伪代码应说明我的需求。

def function():
    return missing_var

try:
    print function()

except NameError:

    frame = inspect.trace()[-1][0]

    # inject missing variable
    frame.f_globals["missing_var"] = ...

    # continue frame execution from last attempted instruction
    exec frame.f_code from frame.f_lasti

在repl.it上阅读整个unittest 

笔记

背景

代码在从属进程中运行,该进程由父进程控制。任务(函数确实)写在父级中,后者使用传递给从站 莳萝。我希望一些任务(在slave进程中运行)尝试从父进程中的外部作用域访问变量,并且我希望slave能够动态地向父进程请求这些变量。

p.s。:我不希望这种魔法在生产环境中运行。


11061
2017-12-22 22:16


起源

Python异常处理模型不允许您这样做,因为必须解开堆栈以确定谁处理异常,并且您不能从必须返回以展开堆栈的任何C函数取消返回。您必须在生成NameError时处理此问题。 - user2357112
我不认为你在python中想要做的是什么,因为当弹出堆栈帧时,本地状态将会丢失,并且将无法恢复它。想要这样做的驱动程序是什么? - dolan
非常感谢您的考虑。 @dolan,此代码在从属进程中运行,该进程由父进程控制。任务写在父级中,后者使用传递给从站 莳萝。我希望一些任务(在slave进程中运行)尝试访问外部变量(在父进程中定义),我希望slave在需要时动态请求这些变量。 - Jules Baratoux
@JulesBaratoux:NameErrors不是处理它的好方法。为什么不让slave进程通过一个惰性映射来访问这些值,这个映射根据需要从父进程请求值,或者显式地向slave发送它需要的值,或者其他一些不需要糟糕的,被排除在外的for-a在例外之后恢复的每种语言的原因是什么? - user2357112
@ user2357112:正如ivan_pozdeev所解释的那样,我目前将这些变量作为参数传递,以避免“可怕”的魔法:D。我将看一下延迟映射。谢谢你的建议! - Jules Baratoux


答案:


与各种评论者所说的相反,“恢复错误”异常处理  在Python中可能。图书馆 fuckit.py 实施所述策略。通过在导入时重写模块的源代码来阻止压路器错误, 插入 try...except 阻止每个陈述 并吞下所有例外。那么也许你可以尝试类似的策略?

不言而喻:那个图书馆是一个笑话。不要在生产代码中使用它。


您提到您的用例是捕获对缺少名称的引用。您是否考虑过使用元编程在“智能”命名空间的上下文中运行代码,例如 defaultdict? (这可能只是一个不好的想法 fuckit.py。)

from collections import defaultdict

class NoMissingNamesMeta(type):
    @classmethod
    def __prepare__(meta, name, bases):
        return defaultdict(lambda: "foo")

class MyClass(metaclass=NoMissingNamesMeta):
    x = y + "bar"  # y doesn't exist

>>> MyClass.x
'foobar'

NoMissingNamesMeta 是一个 元类  - 用于自定义行为的语言构造 class 声明。我们在这里使用 __prepare__ 自定义字典的方法,该字典将在创建类时用作类的命名空间。因此,因为我们正在使用 defaultdict 而不是常规字典,一个其元类是的类 NoMissingNamesMeta 永远不会得到 NameError。在创建类时引用的任何名称都将自动初始化为 "foo"

这种方法类似于@AndréFratelli关于手动请求来自a的延迟初始化数据的想法 Scope 目的。在制作中,我会这样做,而不是这个。元类版本需要较少的输入来编写客户端代码,但代价是更多魔法。 (想象一下你自己在两年内调试这段代码,试图理解为什么不存在的变量被动态地引入范围!)


3
2017-12-23 00:46



这样的图书馆名称!你最后的建议似乎与AndréFratelli的答案相吻合。我很期待尝试相关的东西。谢谢! - Jules Baratoux
实际上并没有“在抛出异常的同一个地方”恢复,而是在下一个声明 - 在“预定的地方”,就像我说的那样。 - ivan_pozdeev


事实证明,“恢复”异常处理技术存在问题,这就是为什么它缺少C ++和后来的语言。

你最好的选择是用一个 while 循环不恢复抛出异常的位置,而是从预定位置重复:

while True:
    try:
        do_something()
    except NameError as e:
        handle_error()
    else:
        break

3
2017-12-22 23:43



while循环是我的第一选择。正如最近补充的解释 背景 部分,目标是提供从父进程动态请求外部作用域中的变量的能力。在我看来,从外部作用域检索变量的能力是加号,但是目前,这些变量作为参数传递,避免了那种魔法的需要。谢谢你的链接顺便说一句,Stroustrup引用总是受欢迎! - Jules Baratoux
@JulesBaratoux这要求挂钩检索过程, 就像安德烈说的那样。试图解决问题 在一份声明中 和 在同一帧中 什么时候 该语言仅允许帧中语句粒度的流控制 按照这个定义,是一项绝对不可能完成的任务。您必须在帧堆栈上处理较低的错误。 - ivan_pozdeev
@JulesBaratoux ...或在需要流控制的位置拆分语句。 - ivan_pozdeev


在抛出异常后你真的无法展开堆栈,所以你必须事先处理这个问题。如果您的要求是动态生成这些变量(不建议这样做,但您似乎明白这一点),那么您必须实际请求它们。您可以为此实现一种机制(例如具有全局自定义 Scope 类实例和重写 __getitem__,或使用类似的东西 __dir__ 功能),但不是你要求它。


3
2017-12-23 00:37



听起来我应该避免异常情况,而不是神秘地处理它。我喜欢 __getitem__ 做法。谢谢! - Jules Baratoux