Item 20: Prefer Raising Exceptions to Returning None

When writing utility functions, there’s a draw for Python programmers to give special meaning to the return value of None. It seems to make sense in some cases. For example, say I want a helper function that divides one number by another. In the case of dividing by zero, returning None seems natural because the result is undefined:

在编写实用程序函数时,Python程序员会给None的返回值赋予特殊的含义。在某些情况下,这似乎是有道理的。例如,假设我想要一个辅助函数来将一个数除以另一个数。在除0的情况下,返回None似乎很自然,因为结果是未定义的:

def careful_divide (a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None

Code using this function can interpret the return value accordingly:

使用这个函数的代码可以相应地解释返回值:

x, y = 1, 0
result = careful_divide (x, y)
if result is None:
    print ( 'Invalid inputs ')

What happens with the careful_divide function when the numerator is zero? If the denominator is not zero, the function returns zero. The problem is that a zero return value can cause issues when you evaluate the result in a condition like an if statement. You might accidentally look for any False-equivalent value to indicate errors instead of only looking for None (see Item 5: "Write Helper Functions Instead of Complex Expressions" for a similar situation):

当分子为零时,careful_divide函数会发生什么?如果分母不为零,则函数返回零。问题是,当您在条件(如if语句)中计算结果时,零返回值可能会导致问题。你可能会寻找任何与false等价的值来指示错误,而不是只寻找None(类似的情况,请参见第5条:“Write Helper Functions Instead of Complex Expressions”):

x, y = 0, 5
result = careful_divide (x, y)
if not result:
    print ( 'Invalid inputs ') # This runs ! But shouldn 't

>>>
Invalid inputs

This misinterpretation of a False-equivalent return value is a common mistake in Python code when None has special meaning. This is why returning None from a function like careful_divide is error prone. There are two ways to reduce the chance of such errors.

当None具有特殊含义时,这种对false等效返回值的误解是Python代码中的一个常见错误。这就是为什么从careful_divide这样的函数返回None很容易出错的原因。有两种方法可以减少此类错误的发生。

The first way is to split the return value into a two-tuple (see Item 19: "Never Unpack More Than Three Variables When Functions Return Multiple Values" for background). The first part of the tuple indicates that the operation was a success or failure. The second part is the actual result that was computed:

第一种方法是将返回值拆分为一个二元组(参见第19项:“Never Unpack More Than Three Variables When Functions Return Multiple Values”)。元组的第一部分表示操作成功或失败,第二部分是计算出的实际结果:

def careful_divide (a, b):
    try:
        return True, a / b
    except ZeroDivisionError:
        return False, None

Callers of this function have to unpack the tuple. That forces them to consider the status part of the tuple instead of just looking at the result of division:

此函数的调用者必须解包元组。这迫使他们考虑元组的状态部分,而不是只看除法的结果:

success, result = careful_divide (x, y)
if not success:
    print ( 'Invalid inputs ')

The problem is that callers can easily ignore the first part of the tuple (using the underscore variable name, a Python convention for unused variables). The resulting code doesn’t look wrong at first glance, but this can be just as error prone as returning None:

问题是,调用者很容易忽略元组的第一部分(使用下划线变量名,这是Python对未使用变量的习惯用法)。结果代码乍一看不会出错,但这可能和返回None一样容易出错:

_, result = careful_divide (x, y)
if not result:
    print ( 'Invalid inputs ')

The second, better way to reduce these errors is to never return None for special cases. Instead, raise an Exception up to the caller and have the caller deal with it. Here, I turn a ZeroDivisionError into a ValueError to indicate to the caller that the input values are bad (see Item 87: "Define a Root Exception to Insulate Callers from APIs" on when you should use Exception subclasses) :

第二种更好的减少这些错误的方法是在特殊情况下也绝不返回None。相反,向调用者抛出一个Exception并让调用者处理它。在这里,我将ZeroDivisionError转换为ValueError,以指示调用者输入的值是错误的(关于何时应该使用Exception子类,请参见Item 87:“Define a Root Exception to Insulate Callers from APIs”):

def careful_divide (a, b):
    try:
       return a / b
    except ZeroDivisionError as e:
        raise ValueError ( 'Invalid inputs ')

The caller no longer requires a condition on the return value of the function. Instead, it can assume that the return value is always valid and use the results immediately in the else block after try (see Item 65: "Take Advantage of Each Block in try/except/else/finally" for details) :

调用者不再依赖于函数返回值这个条件。相反,它可以假设返回值总是有效的,并在try之后立即在else块中使用结果(详见Item 65:“Take Advantage of Each block in try/except/else/finally”)。

x, y = 5, 2
try:
    result = careful_divide (x, y)
except ValueError:
    print ( 'Invalid inputs ')
else:
    print ( 'Result is %.1f ' % result)

>>>
Result is 2.5

This approach can be extended to code using type annotations (see Item 90: "Consider Static Analysis via typing to Obviate Bugs" for background). You can specify that a function’s return value will always be a float and thus will never be None. However, Python’s gradual typing purposefully doesn’t provide a way to indicate when exceptions are part of a function’s interface (also known as checked exceptions). Instead, you have to document the exception-raising behavior and expect callers to rely on that in order to know which Exceptions they should plan to catch (see Item 84: "Write Docstrings for Every Function, Class, and Module").

这种方法可以扩展为使用类型注释代码(请参见第90条:“Consider Static Analysis via typing to Obviate Bugs”)。你可以指定一个函数的返回值将永远是一个浮点数,因此永远不会是None。然而,Python的渐进式类型故意没有提供一种方法来指示异常何时是函数接口的一部分(也称为受控异常)。相反,你必须记录引发异常的行为,并期望调用者依赖于它,以便知道他们应该捕捉哪些异常(参见第84项:“Write Docstrings for Every Function, Class, and Module”)。

Pulling it all together, here’s what this function should look like when using type annotations and docstrings:

综合起来,当使用类型注释和文档字符串时,这个函数应该是这样的:

def careful_divide (a: float, b: float) -> float:
    """
    Divides a by b.
    Raises:
        ValueError: When the inputs cannot be divided.
    """
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError ( 'Invalid inputs ')

Now the inputs, outputs, and exceptional behavior is clear, and the chance of a caller doing the wrong thing is extremely low.

现在输入、输出和异常行为都很清楚了,这样调用者做错事情的几率就非常低。

Things to Remember
要记住的事

✦ Functions that return None to indicate special meaning are error prone because None and other values (e.g., zero, the empty string) all evaluate to False in conditional expressions.
✦ Raise exceptions to indicate special situations instead of returning None. Expect the calling code to handle exceptions properly when they’re documented.
✦ Type annotations can be used to make it clear that a function will never return the value None, even in special situations.

✦ 函数返回None值来表示特殊含义非常容易出错,因为None和其他值(例如: 零、空字符串)在条件表达式中都被计算为False。
✦ 表示特殊情况时使用抛出异常而不是返回None,在文档中记录异常,并期望调用代码能够正确地处理异常。
✦ 可以使用类型注释来明确即使在特殊情况下,函数永远不会返回None。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容