如何实现Python异常的“恢复语义”?

Exp*_*ple 7 python exception

我有一个验证器类,其方法执行多次检查并可能引发不同的异常:

class Validator:
    def validate(something) -> None:
        if a:
            raise ErrorA()
        if b:
            raise ErrorB()
        if c:
            raise ErrorC()
Run Code Online (Sandbox Code Playgroud)

在外部(调用者)代码中有一个地方,我想自定义其行为并防止ErrorB被引发,而不阻止ErrorC. 像恢复语义这样的东西在这里会很有用。然而,我还没有找到实现这一目标的好方法。

澄清一下:我可以控制Validator源代码,但更喜欢尽可能保留其现有的界面。

我考虑过的一些可能的解决方案:

  1. 显而易见的

    try:
        validator.validate(something)
    except ErrorB:
        ...
    
    Run Code Online (Sandbox Code Playgroud)

    不好,因为它也会抑制同时和 都应该提高的ErrorC情况。ErrorBErrorC

  2. 复制粘贴该方法并删除检查:

    # In the caller module
    
    class CustomValidator(Validator):
        def validate(something) -> None:
            if a:
                raise ErrorA()
            if c:
                raise ErrorC()
    
    Run Code Online (Sandbox Code Playgroud)

    a重复and的逻辑c是一个坏主意,如果改变的话会导致错误Validator

  3. 将方法拆分为单独的检查:

    class Validator:
        def validate(something) -> None:
            self.validate_a(something)
            self.validate_b(something)
            self.validate_c(something)
    
        def validate_a(something) -> None:
            if a:
                raise ErrorA()
    
        def validate_b(something) -> None:
            if b:
                raise ErrorB()
    
        def validate_c(something) -> None:
            if c:
                raise ErrorC()
    
    # In the caller module
    
    class CustomValidator(Validator):
        def validate(something) -> None:
            super().validate_a(something)
            super().validate_c(something)
    
    Run Code Online (Sandbox Code Playgroud)

    这只是一个稍微好一点的复制粘贴。如果validate_d()稍后添加一些,我们就会出现错误CustomValidator

  4. 手动添加一些抑制逻辑:

    class Validator:
        def validate(something, *, suppress: list[Type[Exception]] = []) -> None:
            if a:
                self._raise(ErrorA(), suppress)
            if b:
                self._raise(ErrorB(), suppress)
            if c:
                self._raise(ErrorC(), suppress)
    
        def _raise(self, e: Exception, suppress: list[Type[Exception]]) -> None:
            with contextlib.suppress(*suppress):
                raise e
    
    Run Code Online (Sandbox Code Playgroud)

    这就是我目前所倾向于的。有一个新的可选参数,raise语法变得有点难看,但这是可以接受的成本。

  5. 添加禁用某些检查的标志:

    class Validator:
        def validate(something, *, check_a: bool = True,
                     check_b: bool = True, check_c: bool = True) -> None:
            if check_a and a:
                raise ErrorA()
            if check_b and b:
                raise ErrorB()       
            if check_c and c:
                raise ErrorC()
    
    Run Code Online (Sandbox Code Playgroud)

    这很好,因为它允许精细地控制不同的检查,即使它们引发相同的异常。

    然而,它感觉很冗长,并且随着变化需要额外的维护Validator。实际上我那里有超过三张支票。

  6. 按值产生异常:

    class Validator:
        def validate(something) -> Iterator[Exception]:
            if a:
                yield ErrorA()
            if b:
                yield ErrorB()
            if c:
                yield ErrorC()
    
    Run Code Online (Sandbox Code Playgroud)

    这很糟糕,因为这对于现有调用者来说是一个重大更改,并且使得传播异常(典型用途)变得更加冗长:

    # Instead of
    # validator.validate(something)
    
    e = next(validator.validate(something), None)
    if e is not None:
        raise e
    
    Run Code Online (Sandbox Code Playgroud)

    即使我们保持一切向后兼容

    class Validator:
        def validate(something) -> None:
            e = next(self.iter_errors(something), None)
            if e is not None:
                raise e
    
        def iter_errors(something) -> Iterator[Exception]:
            if a:
                yield ErrorA()
            if b:
                yield ErrorB()
            if c:
                yield ErrorC()
    
    Run Code Online (Sandbox Code Playgroud)

    新的抑制调用者仍然需要编写所有这些代码:

    exceptions = validator.iter_errors(something)
    e = next(exceptions, None)
    if isinstance(e, ErrorB):
        # Skip ErrorB, don't raise it.
        e = next(exceptions, None)
    if e is not None:
        raise e
    
    Run Code Online (Sandbox Code Playgroud)

    与前两个选项相比:

    validator.validate(something, suppress=[ErrorB])
    
    Run Code Online (Sandbox Code Playgroud)
    validator.validate(something, check_b=False)
    
    Run Code Online (Sandbox Code Playgroud)

Mis*_*agi 3

除了极少数例外,您正在寻找错误的工具来完成这项工作。在Python中,raise异常意味着执行遇到了无法恢复的异常情况。终止中断的执行是异常的明确目的。

\n
\n

执行模型:4.3。例外情况

\n

Python 使用 \xe2\x80\x9ctermination\xe2\x80\x9d 错误处理模型:异常处理程序可以找出发生的情况并在外层继续执行,但无法修复错误原因并重试失败的操作(除非从顶部重新输入有问题的代码段)。

\n
\n

要获取异常处理的恢复语义,您可以查看用于恢复处理的通用工具。

\n
\n

恢复:协程

\n

Python 的恢复模型是协程yield协程生成器或async协程都允许暂停和显式恢复执行。

\n
def validate(something) -> Iterator[Exception]:\n    if a:\n        yield ErrorA()\n    if b:\n        yield ErrorB()\n    if c:\n        yield ErrorC()\n
Run Code Online (Sandbox Code Playgroud)\n

区分send风格的“适当”协程和迭代器风格的“生成器”协程非常重要。只要不需要将任何值发送到协程,它在功能上就等同于迭代器。Python 对使用迭代器有良好的内置支持:

\n
def validate(something) -> Iterator[Exception]:\n    if a:\n        yield ErrorA()\n    if b:\n        yield ErrorB()\n    if c:\n        yield ErrorC()\n
Run Code Online (Sandbox Code Playgroud)\n

类似地,我们可以filter使用迭代器或使用推导式。迭代器可以轻松地组合并优雅地终止,使它们适合迭代异常情况。

\n
\n

效果处理

\n

异常处理只是更通用效果处理的常见用例。虽然 Python 没有内置的效果处理支持,但仅处理效果的起源或接收器的简单处理程序可以建模为函数:

\n
for e in validator.iter_errors(something):\n    if isinstance(e, ErrorB):\n        continue  # continue even if ErrorB happens\n    raise e\n
Run Code Online (Sandbox Code Playgroud)\n

这允许调用者通过提供不同的处理程序来更改效果处理。

\n
def default_handler(failure: BaseException):\n    raise failure\n\ndef validate(something, failure_handler = default_handler) -> None:\n    if a:\n        failure_handler(ErrorA())\n    if b:\n        failure_handler(ErrorB())\n    if c:\n        failure_handler(ErrorC())\n
Run Code Online (Sandbox Code Playgroud)\n

这对于依赖倒置来说似乎很熟悉,并且实际上与之相关。

\n

购买效果处理有多个阶段,并且可以通过类重现大部分(如果不是全部)功能。除了技术功能之外,还可以通过线程局部变量或上下文局部变量来实现环境效果处理程序(类似于自动try“连接”的方式raise)。

\n