>>> from env_helper import info; info()
页面更新时间: 2023-06-30 19:31:15
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

3.2. 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:

>>> 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')
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):

>>> 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.

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:

>>> 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:

>>> _, result = careful_divide(x, y)
>>> if not result:
>>>     print('Invalid inputs')
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):

>>> 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):

>>> 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”).

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.

3.2.1. 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.