LBYL 的意思是“Look before you leap.” 指在程序执行之前做好检查。比如下面这段代码:
if "key" in my_dict: x = my_dict["key"] else: # handle missing key
EAFP 的意思是“Easier to ask for forgiveness than permission.” 在编程方面指的是相信程序会正确执行,如果出错了再处理错误,比如上面这段代码用 EAFP 风格写就是下面这样:
try: x = my_dict["key"] except KeyError: # handle missing key
很多情况下,两种方式的写法是可以互相替换的,但是 Python 鼓励 EAFP 。原因是这种方法的可读性更高,速度也更快(只有在出错的时候才需要处理,而 LBYL 需要每次运行都检查)。
现在的大多数语言都有异常处理机制了,比如 Java, Python, Ruby 等,没有异常处理机制的都是一些古老的语言,比如 C 语言。但是没有异常处理机制并不代表程序总能运行正确,所以 C 语言需要其他形式来处理程序(函数)不能正确运行的情况,一般的方法是返回 0
表示运行正常,其他值表示运行异常。
但是这种处理形式有两个缺点(这部分可以参考《代码之髓》这本书的第 6 章):第一是可能遗漏错误。比如一个函数在大部分情况下都能正确运行,只有在很少的情况下会出错,或者只有改变了环境之后才会出错。那么很可能程序员会默认这个函数总是正确的,而忘记处理这个异常。如果是 Java 的话,程序员就必须在调用函数的地方处理掉所有可能的异常(虽然很多 Java 程序员喜欢不优雅地用 RuntimeError 处理所有的异常)。更让人头疼的事情是,一旦这“极少数”的情况发生了,那么错误经常不在错误出现的地方,而在很外层的一个调用处。你要花很多时间调试才能找到最终出错的地方。如果有异常处理机制的话,异常栈通常会直接给出 Exception 发生的地方。
第二个缺点是会使代码的可读性下降。因为要检查函数的返回值,要写很多 if-else
,程序真正的逻辑就变得难以阅读。我记得有位高人说过这么一句话,具体是谁说的记不得了,大意是:“高级语言和低级语言的区别是,需要不需要写很多与程序的逻辑无关的东西。” 很多 if-else
,很难看出这个只是判断,还是程序逻辑/业务的判断。如果用 try-catch
,那么 try
代码块里面可以只写程序的逻辑,在 except
里面处理所有的异常。
即使在有异常处理机制的语言中,比如 Python,很多人喜欢做的一个事情是在子函数用 if 判断,然后 logger.error + return。其实不如 raise ,然后在调用者那里 try-catch,更能表达逻辑。
if not monitor_config.link: logger.error(f'{self} does not associate with link') return
此外,Python 语言内置的协议也大量使用了异常的机制。比如《fluent python》(314页)关于重载加运算符 __add__
就提到,为了遵守鸭子精神,不要测试 other 操作数的类型,而是应该捕获异常,然后抛出 NotImplemented
。
这样的好处是,比如我们定义了一个新的数据类型,支持和 int
相加 a.__add__(4)
。但是内置的 int
可不支持和我们自定义的类型相加, 4.__add__(a)
就会抛出异常。这时解释器尝试用 __radd__
来处理(即 a.__add__(4)
)。如果 int
的 __add__
是实现是相加对象的类型,如果不符合预期就抛出一个 TypeError
,就没有这样的便利了。
但是 EAFP 在某些情况下可能是不可行的,它的一个问题就是等错误发生的时候,程序已经运行了一半,如果函数会造成一些副作用,那么这个时候副作用已经发生了。这种情况下,如果没有数据事务这种外部的东西来提供原子性的话,就比较麻烦了,需要手动清理副作用的状态。而 LBFY 这种风格可以尽量保证提供给程序的参数正确,可以顺利运行完成。
其实我想讨论这二者的区别的真正地方是,在生活中也会有这两种风格的处理方式。
团队管理上 try-catch 更自由,更人性化,大家可以关注自己“做事的逻辑”而没有很多条条框框,在程序上 if 可能就多了一次判断,但是在实际生活中的话,这种 if 可能是各种各样的沟通和 ask permission,效率可就不只低这么多了。但是换句话来说,严谨的系统容不得做到半路才出现 “Exception”,还是需要“Look before you leap”的。
参考资料: