跳转到主要内容

从Common Lisp的基于重启的条件系统的一个Python化版本

项目描述

withrestart:使用命名重启函数的 structured error recovery

这是一个Common Lisp的基于重启的条件系统的Python化版本(Lispers可能会正确地说“混血儿”)。它通过去除未处理错误必须是致命的假设,旨在使错误恢复更加简单和容易。

“重启”代表在错误发生之后继续执行函数的命名策略。在执行过程中,任何函数都可以将其Restart对象推送到调用栈上。如果在Restart的作用域内发生异常,调用链中的更高层代码可以调用它以从错误中恢复并让函数继续执行。通过提供多个重启,函数可以提供多种从错误中恢复的策略。

“处理器”代表处理错误发生的高级策略。在概念上与“except”子句相似,因为它们都会在执行某些代码时,建立一系列Handler对象以在发生错误时调用。然而,有一个关键的区别:处理器在不需要回溯调用栈的情况下执行。因此,它们有机会采取纠正措施,然后恢复引发错误的函数的执行。

例如,考虑一个函数,它将目录中所有文件的内容读取到内存中的字典

def readall(dirname):
    data = {}
    for filename in os.listdir(dirname):
        filepath = os.path.join(dirname,filename)
        data[filename] = open(filepath).read()
    return data

如果在调用 os.listdir() 之后某个文件丢失,则随后的 open() 将引发 IOError。虽然我们可以在该函数内部捕获并处理错误,但适当的操作是什么?应该默默地忽略丢失的文件吗?应该用一些默认内容重新创建它们吗?应该在数据字典中放置一个特殊的哨兵值?应该放置什么值?readall() 函数没有足够的信息来决定适当的恢复策略。

相反,readall() 可以提供执行错误恢复的 基础设施,并将最终决策留给调用代码。以下定义使用三个预定义的重启,让调用代码(a)完全跳过丢失的文件,(2)在采取一些纠正措施后重试 open() 的调用,或者(3)使用其他值代替丢失的文件

def readall(dirname):
    data = {}
    for filename in os.listdir(dirname):
        filepath = os.path.join(dirname,filename)
        with restarts(skip,retry,use_value) as invoke:
            data[filename] = invoke(open,filepath).read()
    return data

值得注意的是,这里使用“with”语句在重启的作用域内创建一个新上下文,以及在调用可能失败的功能时使用“invoke”包装器。后者允许重启向失败的功能注入替代返回值。

以下是调用代码的示例,如果它想要静默地跳过丢失的文件

def concatenate(dirname):
    with Handler(IOError,"skip"):
        data = readall(dirname)
    return "".join(data.itervalues())

这会将一个 Handler 实例推入执行上下文,该实例将检测 IOError 实例并通过调用“skip”重启点来响应。如果此处理程序因 IOError 而被调用,readall() 函数的执行将在“with restarts(…)”块之后立即继续。

请注意,使用常规的 try-except 块无法实现这种跳过并继续的行为;在 IOError 传播到 concatenate() 函数进行处理时,readall() 执行的所有上下文都将被撤销,无法恢复。

如果调用代码想要重新创建丢失的文件,只需推送不同的错误处理程序

def concatenate(dirname):
    def handle_IOError(e):
        open(e.filename,"w").write("MISSING")
        raise InvokeRestart("retry")
    with Handler(IOError,handle_IOError):
        data = readall(dirname)
    return "".join(data.itervalues())

通过引发 InvokeRestart,此处理程序将控制权转回到由 readall() 函数建立的重启。这个特定的重启将重新执行失败的功能调用,并允许 readall() 继续其操作。

如果调用代码想要使用特殊的哨兵值,将使用处理程序将所需值传递给“use_value”重启

def concatenate(dirname):
    class MissingFile:
        def read():
            return "MISSING"
    def handle_IOError(e):
        raise InvokeRestart("use_value",MissingFile())
    with Handler(IOError,handle_IOError):
        data = readall(dirname)
    return "".join(data.itervalues())

通过将错误恢复的低级细节与采取什么行动的高层策略分离,可以创建相当强大的恢复机制。

虽然此模块提供了一些预构建的重启,但函数通常需要创建自己的。这可以通过将回调传递给 Restart 对象构造函数来完成

def readall(dirname):
    data = {}
    for filename in os.listdir(dirname):
        filepath = os.path.join(dirname,filename)
        def log_error():
            print "an error occurred"
        with Restart(log_error):
            data[filename] = open(filepath).read()
    return data

或者通过使用装饰器来定义内联重启

def readall(dirname):
    data = {}
    for filename in os.listdir(dirname):
        filepath = os.path.join(dirname,filename)
        with restarts() as invoke:
            @invoke.add_restart
            def log_error():
                print "an error occurred"
            data[filename] = open(filepath).read()
    return data

处理程序也可以使用类似的语法定义内联

def concatenate(dirname):
    with handlers() as h:
        @h.add_handler
        def IOError(e):
            open(e.filename,"w").write("MISSING")
            raise InvokeRestart("retry")
        data = readall(dirname)
    return "".join(data.itervalues())

现在最后,一个免责声明。我从未写过任何 Common Lisp。我只阅读过关于 Common Lisp 条件系统及其如何酷炫的内容。我确信有很多事情它可以做,而这个模块简单地无法做到。例如

  • 由于这是建立在标准异常抛出系统之上,处理程序只能在堆栈撤销到最近的重启上下文之后执行;在 Common Lisp 中,它们在完全不撤销堆栈的情况下执行。

  • 由于这是建立在标准异常抛出系统之上,它可能太重量级,不适合用作通用条件信号系统。

尽管如此,当你看到一个好主意时,窃取它是没有羞耻的……

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源代码分发

withrestart-0.2.7.tar.gz (13.5 kB 查看哈希值)

上传时间 源代码

由以下组织支持