“请求原谅”和“三思而后行”(有时也称为“请求许可”)是编写代码的两种相反的方法。如果你“三思而后行”,你首先要检查一切是否设置正确,然后再执行一个操作。例如,您要从文件中读取文本。那会出什么问题呢?好吧,文件可能不在您期望的位置。因此,首先检查文件是否存在:
import os
if os.path.exists("path/to/file.txt"):
...
# Or from Python 3.4
from pathlib import Path
if Path("/path/to/file").exists():
...
即使文件存在,也许你没有打开它的权限?所以让我们看看你是否能读到:
import os
if os.access("path/to/file.txt", os.R_OK):
...
但是如果文件损坏了怎么办?或者你没有足够的内存去读它?这个名单可以继续下去。最后,当你认为你检查了每一个可能的问题,你可以打开并阅读它:
with open("path/to/file.txt") as input_file:
return input_file.read()
根据您要执行的操作,可能需要执行很多检查。即使你认为你已经涵盖了所有内容,也不能保证某些意外的问题不会阻止你阅读此文件。所以,你可以“请求原谅”,而不是做所有的检查
有了“请求原谅”,你什么都不检查。您可以执行您想要的任何操作,但将其包装在try/catch块中。如果发生异常,则处理它。您不必考虑所有可能出错的地方,您的代码要简单得多(没有更多嵌套的ifs),而且这样通常会捕获更多的错误。这就是为什么Python社区通常更喜欢这种方法,通常称为“EAFP”——“请求原谅比请求许可更容易。”
下面是一个使用“请求原谅”方法读取文件的简单示例:
try:
with open("path/to/file.txt", "r") as input_file:
return input_file.read()
except IOError:
# Handle the error or just ignore it
这里我们正在捕捉IOError。如果您不确定可以引发哪种类型的异常,可以使用BaseException类捕捉所有异常,但一般来说,这是一种不好的做法。它将捕捉所有可能的异常(例如,当你想停止进程时,包括KeyboardInterrupt),所以尽量更具体一些。
“请求原谅”更干净。但是哪一个更快?
“请求原谅”与“三思而后行”-速度
是时候做个简单的测试了。假设我有一个类,我想从这个类中读取一个属性。但是我使用的是继承,所以我不确定是否定义了属性。我需要保护自己,要么检查它是否存在(“三思而后行”),要么抓住AttributeError(“请求原谅”):
# permission_vs_forgiveness.py
class BaseClass:
hello = "world"
class Foo(BaseClass):
pass
FOO = Foo()
# Look before you leap
def test_lbyl():
if hasattr(FOO, "hello"):
FOO.hello
# Ask for forgiveness
def test_aff():
try:
FOO.hello
except AttributeError:
pass
我们来测量这两个函数的速度。
$ python -m timeit -s "from permission_vs_forgiveness import test_lbyl" "test_lbyl()"
2000000 loops, best of 5: 155 nsec per loop
$ python -m timeit -s "from permission_vs_forgiveness import test_aff" "test_aff()"
2000000 loops, best of 5: 118 nsec per loop
你先看一下,再慢一点。
如果我们增加检查数量会怎么样?假设这次我们要检查三个属性,而不仅仅是一个:
# permission_vs_forgiveness.py
class BaseClass:
hello = "world"
bar = "world"
baz = "world"
class Foo(BaseClass):
pass
FOO = Foo()
# Look before you leap
def test_lbyl2():
if hasattr(FOO, "hello") and hasattr(FOO, "bar") and hasattr(FOO, "baz"):
FOO.hello
FOO.bar
FOO.baz
# Ask for forgiveness
def test_aff2():
try:
FOO.hello
FOO.bar
FOO.baz
except AttributeError:
pass
$ python -m timeit -s "from permission_vs_forgiveness import test_lbyl2" "test_lbyl2()"
500000 loops, best of 5: 326 nsec per loop
$ python -m timeit -s "from permission_vs_forgiveness import test_aff2" "test_aff2()"
2000000 loops, best of 5: 176 nsec per loop
“三思而后行”现在大约慢了85%(326/176≈1.852)。所以“请求原谅”不仅更容易阅读,而且在很多情况下,速度也更快。是的,你读得对,“在很多情况下,”不是“在所有情况下”!”
“EAFP”和“LBYL”的主要区别
如果属性实际上没有定义,会发生什么?看看这个例子:
# permission_vs_forgiveness.py
class BaseClass:
pass # "hello" attribute is now removed
class Foo(BaseClass):
pass
FOO = Foo()
# Look before you leap
def test_lbyl3():
if hasattr(FOO, "hello"):
FOO.hello
# Ask for forgiveness
def test_aff3():
try:
FOO.hello
except AttributeError:
pass
$ python -m timeit -s "from permission_vs_forgiveness import test_lbyl3" "test_lbyl3()"
2000000 loops, best of 5: 135 nsec per loop
$ python -m timeit -s "from permission_vs_forgiveness import test_aff3" "test_aff3()"
500000 loops, best of 5: 562 nsec per loop
形势已经好转。“请求原谅”现在是“三思而后行”的四倍多(562/135≈4.163)。这是因为这一次,我们的代码抛出了一个异常。而且处理异常的成本很高。
如果您希望您的代码经常失败,那么“三思而后行”可能会快得多。
决断
“请求原谅”的结果是代码更简洁,更容易发现错误,而且在大多数情况下,速度更快。难怪EAFP(“请求原谅比请求许可更容易”)在Python中是如此普遍的模式。即使在本文开头的示例中(检查文件是否存在os.path.exists)-如果您查看exists方法的源代码,就会发现它只是使用try/except。“三思而后行”通常会导致代码较长,可读性较差(使用嵌套的if语句)且速度较慢。按照这种模式,你有时可能会漏掉一两个判断。
请记住,处理异常是缓慢的。扪心自问:“这段代码是否会抛出异常更常见?如果答案是“是”,你可以用一个好的“IF”来解决这些问题,太好了!但在很多情况下,你无法预测你会遇到什么问题。使用“请求原谅”是非常好的-在你开始加速之前,你的代码应该是“正确的”。