第20条 遇到意外状况时应该抛出异常,不要返回None
编写工具函数(utility function)时,许多Python程序员都爱用None
这个返回值来表示特殊情况。对于某些函数来说,这或许有几分道理。例如,我们要编写一个辅助函数计算两数相除的结果。在除数是0的情况下,返回None
似乎相当合理,因为这种除法的结果是没有意义的。
调用这个函数时,可以按自己的方式处理这样的返回值。
如果传给careful_divide
函数的被除数为0,会怎么样呢?在这种情况下,只要除数不为0,函数返回的结果就应该是0。问题是,这个函数的返回值有时可能会用在if
条件语句里面,那时可能会根据值本身是否相当于False
来做判断,而不像刚才那样明确判断这个值是否为None
(第5条也列举了类似的例子)。
上面这种if
语句,会把函数返回0时的情况,也当成函数返回None
时那样来处理。这种写法经常出现在Python代码里,因此像careful_divide
这样,用None
来表示特殊状况的函数是很容易出错的。有两种办法可以减少这样的错误。
第一种办法是,利用二元组把计算结果分成两部分返回(与这种写法有关的基础知识,参见第19条)。元组的首个元素表示操作是否成功,第二个元素表示计算的实际值。
这样写,会促使调用函数者去拆分返回值,他可以先看看这次运算是否成功,然后再决定怎么处理运算结果。
但问题是,有些调用方总喜欢忽略返回元组的第一个部分(在Python代码里,习惯用下划线表示用不到的变量)。这样写成的代码,乍一看似乎没问题,但实际上还是无法区分返回0与返回None
这两种情况。
第二种办法比刚才那种更好,那就是不采用None
表示特例,而是向调用方抛出异常(Exception
),让他自己去处理。下面我们把执行除法时发生的ZeroDivisionError
转化成ValueError
,告诉调用方输入的值不对(什么时候应该使用Exception
类的子类,可参见第87条)。
现在,调用方拿到函数的返回值之后,不用先判断操作是否成功了。因为这次可以假设,只要能拿到返回值,就说明函数肯定顺利执行完了,所以只需要用try
把函数包起来并在else
块里处理运算结果就好(这种结构的详细用法,参见第65条)。
这个办法也可以扩展到那些使用类型注解的代码中(参见第90条),我们可以把函数的返回值指定为float
类型,这样它就不可能返回None
了。然而,Python采用的是动态类型与静态类型相搭配的gradual类型系统,我们不能在函数的接口上指定函数可能抛出哪些异常(有的编程语言支持这样的受检异常(checked exception),调用方必须应对这些异常)。所以,我们只好把有可能抛出的异常写在文档里面,并希望调用方能够根据这份文档适当地捕获相关的异常(参见第84条)。
下面我们给刚才那个函数加类型注解,并为它编写docstring。
这样写,输入、输出与异常都显得很清晰,所以调用方出错的概率就变得很小了。
要点
- 用返回值
None
表示特殊情况是很容易出错的,因为这样的值在条件表达式里面,没办法与0和空白字符串之类的值区分,这些值都相当于False
。 - 用异常表示特殊的情况,而不要返回
None
。让调用这个函数的程序根据文档里写的异常情况做出处理。 - 通过类型注解可以明确禁止函数返回
None
,即便在特殊情况下,它也不能返回这个值。