异常传播装饰器是一个好的模式吗?

gil*_*gil 6 python python-decorators

明确定义的自定义异常通常比内置异常提供更多信息;例如AgeError,超过ValueError。所以一般来说,我会尽可能使用前者。但因此,我的代码中散布着大量raise foo from bar样板文件,只是为了传播自定义异常。这是我的意思的一个例子。如果不使用自定义异常,我只需编写:

class Person:
    def set_age(self, age_as_string):
        self.age = int(age_as_string)
Run Code Online (Sandbox Code Playgroud)

这可能会引发TypeErrorValueError,但由于调用者会处理它,所以单行就可以了。

但要使用自定义异常,我需要样板:

class AgeError(Exception):
    pass

class Person:
    def set_age(self, age_as_string):
        try:
            self.age = int(age_as_string)
        except (TypeError, ValueError) as e:
            raise AgeError from e
Run Code Online (Sandbox Code Playgroud)

从调用者的角度来看,这提供了更多信息,但代码成本增加了 300%(仅计算方法体),并且模糊了set_age.

有没有办法两全其美?我尝试在谷歌上搜索解决方案,但即使这个问题似乎也没有得到太多讨论。我最终找到的解决方案是使用异常传播装饰器,由于其出色的功能,编写起来很简单contextlib(如果您需要手动实现它,则稍微不那么简单):

from contextlib import contextmanager

@contextmananer
def raise_from(to_catch, to_raise):
    try:
        yield
    except to_catch as e:
        raise to_raise from e
Run Code Online (Sandbox Code Playgroud)

现在我只需要额外一行,这不会掩盖业务逻辑,甚至使错误处理逻辑更加明显(而且看起来很聪明):

class Person:
    @raise_from(to_catch=(TypeError, ValueError), to_raise=AgeError)
    def set_age(self, age_as_string):
        self.age = int(age_as_string)
Run Code Online (Sandbox Code Playgroud)

所以我对这个解决方案非常满意。但由于像这样的简单解决方案不太可能仍然存在任何未解决的问题,所以我担心我可能会错过一些东西。使用raise_from装饰器是否有我没有考虑到的缺点?或者,减少样板文件的必要性raise foo from bar是否表明我做错了什么?

blh*_*ing 3

您的上下文管理器没有任何问题raise_from,但值得指出的一个小问题是您将其用作函数装饰器,它将异常映射逻辑应用于函数的整个主体。set_age当函数不是像示例中的方法那样的单行函数时,这可能会出现问题,因为函数中的多个语句可能会因不同的原因引发相同的异常。

AgeError例如,当以下原因引起时,您绝对不想被加薪int(weight_as_string)

class Person:
    @raise_from(to_catch=(TypeError, ValueError), to_raise=AgeError)
    def __init__(self, age_as_string, weight_as_string):
        self.age = int(age_as_string)
        self.weight = int(weight_as_string)
Run Code Online (Sandbox Code Playgroud)

因此,为了使异常处理程序的范围尽可能缩小,通常最好使用raise_from上下文管理器而不是装饰器:

class Person:
    def __init__(self, age_as_string, weight_as_string):
        with raise_from(to_catch=(TypeError, ValueError), to_raise=AgeError):
            self.age = int(age_as_string)
        with raise_from(to_catch=(TypeError, ValueError), to_raise=WeightError):
            self.weight = int(weight_as_string)
Run Code Online (Sandbox Code Playgroud)