关于Python:”请求宽恕而非许可”-解释

“Ask forgiveness not permission” - explain

我不是在问关于这一哲学的个人"宗教"观点,而是一些更技术的东西。

我理解这个短语是几个石蕊测试之一,看看你的代码是否是"Python"。但对我来说,Python意味着干净、简单和直观,而不是为糟糕的编码加载异常处理程序。

所以,举个实际的例子。我定义了一个类:

1
2
3
4
5
6
7
class foo(object):
    bar = None

    def __init__(self):
        # a million lines of code
        self.bar ="Spike is my favorite vampire."
        # a million more lines of code

现在,从程序背景来看,在另一个函数中,我想这样做:

1
2
if foo.bar:
    # do stuff

如果我不耐烦并且没有执行初始foo=none,我将得到一个属性异常。所以,"请求原谅而不是允许"建议我应该这样做?

1
2
3
4
5
try:
    if foo.bar:
        # do stuff
except:
    # this runs because my other code was sloppy?

为什么我最好在一个try块中添加额外的逻辑,这样我就可以使类定义更加模糊?为什么不先定义所有内容,然后明确地授予权限?

(不要因为使用Try/Except块而痛扁我…我到处都用。我只是觉得用它们来捕捉我自己的错误是不对的,因为我不是一个彻底的程序员。)

或者…我是否完全误解了"请原谅"这句咒语?


"请求原谅,而不是许可"反对两种编程风格。"请求许可"如下:

1
2
3
4
if can_do_operation():
    perform_operation()
else:
    handle_error_case()

"请求宽恕"是这样的:

1
2
3
4
try:
    perform_operation()
except Unable_to_perform:
    handle_error_case()

在这种情况下,预期尝试执行操作可能会失败,您必须以某种方式处理操作不可能的情况。例如,如果操作正在访问文件,则该文件可能不存在。

请求宽恕有两个主要原因:

  • 在同一个世界中(在多线程程序中,或者如果操作涉及到程序外部的对象,如文件、其他进程、网络资源等),运行EDOCX1时(0)和运行EDOCX1时(1)之间的情况可能会发生变化。所以无论如何,你必须处理这个错误。
  • 你需要使用正确的标准来请求许可。如果你弄错了,你要么无法执行你可以执行的操作,要么因为你根本无法执行操作而出错。例如,如果在打开文件之前测试该文件是否存在,则可能该文件确实存在,但由于您没有权限,因此无法打开该文件。相反,文件可能是在打开时创建的(例如,因为它通过网络连接,而网络连接仅在实际打开文件时才会打开,而不是仅在插入查看文件是否存在时打开)。

请求宽恕的情况有一个共同点,那就是你试图执行一个操作,你知道这个操作可能会失败。

当你写foo.bar时,bar的不存在通常不被认为是对象foo的失败。这通常是一个程序员的错误:试图以一种不是为之设计的方式使用一个对象。在python中,程序员错误的结果是一个未处理的异常(如果幸运的话:当然,一些程序员错误不能被自动检测到)。因此,如果bar是对象的可选部分,处理这一问题的正常方法是将bar字段初始化为None字段,如果可选部分存在,则将其设置为其他值。要测试是否存在bar,请写入

1
2
3
4
if foo.bar is not None:
     handle_optional_part(foo.bar)
else:
     default_handling()

只有当bar在解释为布尔值时始终为真时,才可以将if foo.bar is not None:缩写为if foo.bar:,如果bar可以是0、[]{}或任何其他具有假真值的对象,则需要is not None。如果您测试的是可选部件(而不是TrueFalse之间的测试),这一点也更清楚。

此时,您可能会问:为什么不在不存在时省略bar的初始化,并用hasattr测试它的存在,或者用AttributeError处理程序捕获它?因为您的代码只有在两种情况下才有意义:

  • 对象没有bar字段;
  • 对象有一个bar字段,表示您认为它的含义。

因此,在编写或决定使用对象时,您需要确保它没有具有不同含义的bar字段。如果您需要使用一些没有bar字段的不同对象,这可能不是您唯一需要适应的对象,因此您可能需要创建派生类或将对象封装到另一个类中。


经典的"请求宽恕而非许可"例子是从可能不存在的dict访问值。例如。:

1
2
3
4
5
6
7
names = { 'joe': 'Joe Nathan', 'jo': 'Jo Mama', 'joy': 'Joy Full' }
name = 'hikaru'

try:
    print names[name]
except KeyError:
    print"Sorry, don't know this '{}' person".format(name)

这里提到了可能发生的例外(KeyError),这样您就不会要求原谅可能发生的每一个错误,而是只要求自然发生的错误。相比之下,"先请求权限"方法可能如下所示:

1
2
3
4
if name in names:
    print names[name]
else:
    print"Sorry, don't know this '{}' person".format(name)

1
2
3
4
5
real_name = names.get(name, None)
if real_name:
    print real_name
else:
    print"Sorry, don't know this '{}' person".format(name)

这种"请求宽恕"的例子往往过于简单。在我看来,不清楚的是,tryexcept块本质上比ifelse块好。在执行可能以各种方式失败的操作时(如解析、使用eval()、访问操作系统、中间件、数据库或网络资源,或执行复杂的数学运算),实际值要清楚得多。当有多种潜在的失败模式时,准备好获得原谅是非常有价值的。

有关代码示例的其他注释:

您不需要在每个变量使用周围添加try/except块。那太可怕了。你不需要在你的__init__()中设置self.bar,因为它是在你的class定义中设置的。通常在类(如果它的数据可能在类的所有实例之间共享)或__init__()中定义它(如果它是实例数据,特定于每个实例)。

顺便说一下,None的值没有定义,也没有错误。它是一个特定的合法值,意味着无、零、空或无。许多语言都有这样的值,因此程序员不会"超载"0-1''(空字符串)或类似的有用值。


你说得对——tryexcept的目的并不是为了掩盖你草率的编码。这只会导致更草率的编码。

异常处理应用于处理异常情况(草率编码不是异常情况)。然而,通常很容易预测会发生哪些异常情况。(例如,您的程序接受用户输入并使用它访问字典,但用户输入不是字典中的键…)


这里有很多好的答案,我只是想补充一点,我到目前为止还没有提到。

通常请求原谅而不是许可会提高性能。

  • 当你请求许可时,你必须执行一个额外的操作每次请求许可。
  • 请求宽恕时,有时你只需要做一个额外的手术,即它失败了。

通常,失败案例很少,这意味着如果你只是请求许可,那么你几乎不需要做任何额外的操作。是的,当它失败时,它抛出一个异常,并执行一个额外的操作,但是Python中的异常非常快。您可以在这里看到一些时间安排:https://jeffknupp.com/blog/2013/02/06/write-cleaner-python-use-exceptions/


虽然已经有许多高质量的答案,但大多数主要是从一个风格的角度来讨论这个问题,因为这与一个功能性的答案是一致的。

在某些情况下,我们需要请求原谅,而不是允许确保正确的代码(在多线程程序之外)。

一个典型的例子是,

1
2
if file_exists:
    open_it()

在本例中,文件可能在检查和尝试实际打开文件之间被删除。这可以通过使用try来避免:

1
2
3
4
try:
    open_it()
except FileNotFoundException:
    pass # file doesn't exist

它出现在许多地方,通常使用文件系统或外部API。


在Python上下文中,"请求原谅而不是许可"意味着一种编程风格,在这种风格中,您不检查事情是否如预期的那样,而是处理错误(如果不是这样的话)。经典的例子是不检查字典是否包含给定的键,如:

1
2
3
4
5
6
d = {}
k ="k"
if k in d.keys():
  print d[k]
else:
  print"key "" + k +"" missing"

但是,如果缺少键,则处理所产生的错误:

1
2
3
4
5
6
d = {}
k ="k"
try:
  print d[k]
except KeyError:
  print"key "" + k +"" missing"

然而,重点并不是用try/except替换代码中的每个if;这将使代码明显变得更混乱。相反,您应该只在真正可以对错误做些什么的地方捕获错误。理想情况下,这将减少代码中的整体错误处理量,从而使其实际用途更加明显。


我个人的非宗教观点是,这句咒语主要适用于记录在案且被充分理解的退出条件和边缘案件(如I/O错误),决不应被用作粗心编程的越狱卡。

也就是说,当存在"更好"的替代方案时,通常使用try/except。例如:

1
2
3
4
5
6
7
8
# Do this    
value = my_dict.get("key", None)

# instead of this
try:
  value = my_dict["key"]
except KeyError:
  value = None

对于您的示例,如果您无法控制foo,并且需要检查是否符合您的预期,请使用if hasattr(foo,"bar"),否则只需使用foo.bar,并让由此产生的错误作为识别和修复松散代码的指南。


请求原谅,而不是许可,是为了简化代码。这意味着当有一个合理的期望.bar可能触发一个属性错误时,代码应该这样编写。

1
2
3
4
 try:
     print foo.bar
 except AttributeError as e
     print"No Foo!"

你的代码似乎既请求许可又请求宽恕:)

问题是,如果您合理地期望某件事情失败,请使用一个尝试/捕获。如果您不希望某个东西失败,而且无论如何失败了,那么抛出的异常就相当于其他语言中失败的断言。您可以看到意外异常发生的位置,并相应地调整代码/假设。