【问题标题】:"Ask forgiveness not permission" - explain“请求宽恕而不是许可” - 解释
【发布时间】:2012-08-29 05:30:08
【问题描述】:

我不是在询问关于这种哲学的个人“宗教”意见,而是更多技术性的意见。

我知道这句话是测试您的代码是否“pythonic”的几个试金石之一。但对我来说,pythonic 意味着干净、简单和直观,没有加载用于糟糕编码的异常处理程序。

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

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

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

if foo.bar:
    # do stuff

如果我不耐烦并且没有执行初始的 foo = None,我会得到一个属性异常。那么,“请求宽恕而不是许可”建议我应该这样做吗?

try:
    if foo.bar:
        # do stuff
except:
    # this runs because my other code was sloppy?

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

(不要因为使用 try/except 块而责备我……我到处都在使用它们。我只是不认为用它们来捕获我自己的错误是正确的,因为我不是一个彻底的程序员。)

或者……我完全误解了“请求宽恕”的口头禅吗?

【问题讨论】:

  • 您的声明“未加载错误编码的异常处理程序”是不合理的。好的代码具有所有必要的异常处理,但仅此而已。异常处理程序的存在几乎不意味着“糟糕的编码”。我认为如果可能的话,最好让应用程序处理问题(如果没有其他原因,只是优雅地关闭),而不是让它在用户的膝上毫不客气地崩溃:)
  • 你不应该尝试添加异常处理程序来捕捉错误的编码,尤其是不是一个简单的except: 子句。你的单元测试应该能捕捉到这些问题,对于这种错误吐出回溯并杀死整个过程的异常并没有错。
  • 请求宽恕的另一个好处是,当您处理无法控制的动态状态时,它有时可以避免权限根本无法解决的错误。我认为典型的例子是os.path.exists,它只告诉你文件在某个时候存在或不存在。可能 90% 以上的 if os.path.exists(filename): do_something_to(filename) 使用在技术上存在问题。
  • @DSM 或只是open() - 不要检查是否可以先打开文件(权限/存在等...) - 试试吧 :) [避免可能的竞争条件]

标签: python


【解决方案1】:

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

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),因此您不必为可能发生的每个错误请求宽恕,而只是针对自然发生的错误请求宽恕。 作为比较,“先请求许可”的方法可能如下所示:

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

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

这种“请求原谅”的例子往往过于简单。 IMO 不清楚try/except 块本质上优于if/else。当执行可能以各种方式失败的操作时,真正的价值要清楚得多——比如解析;使用eval();访问操作系统、中间件、数据库或网络资源;或执行复杂的数学。当存在多种潜在的失败模式时,准备好获得宽恕是非常有价值的。

关于您的代码示例的其他说明:

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

顺便说一句,None 的值不是未定义的,也不是错误。它是一个特定且合法的值,表示 none、nil、null 或无。许多语言都有这样的值,因此程序员不会“重载”0-1''(空字符串)或类似的有用值。

【讨论】:

  • 每个人都有很棒的 cmets。我将此标记为答案,因为它为我阐明了另外两件事:“通常在类中定义它(如果它的数据可能在类的所有实例之间共享)或在 __init__() (如果它是实例数据,特定于每个实例)。”是对我在 OO 编程中是如何工作的一个必要的提醒,并且“当有多种潜在的失败模式时,准备好获得宽恕是非常有价值的。”感谢大家的良好回复,并为我的不良示例感到抱歉。
【解决方案2】:

“请求宽恕,而不是许可”反对两种编程风格。 “请求许可”是这样的:

if can_do_operation():
    perform_operation()
else:
    handle_error_case()

“请求宽恕”是这样的:

try:
    perform_operation()
except Unable_to_perform:
    handle_error_case()

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

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

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

请求宽恕情况的共同点是您正在尝试执行一项操作,并且您知道该操作可能会失败。

当您编写foo.bar 时,bar 的不存在通常不会被视为对象foo 的故障。这通常是程序员的错误:试图以非设计的方式使用对象。 Python 中程序员错误的后果是未处理 异常(如果幸运的话:当然,有些程序员错误无法自动检测到)。因此,如果bar 是对象的可选部分,则处理此问题的正常方法是拥有一个初始化为Nonebar 字段,如果存在可选部分,则设置为其他值。要测试bar 是否存在,请编写

if foo.bar is not None:
     handle_optional_part(foo.bar)
else:
     default_handling()

您可以将 if foo.bar is not None: 缩写为 if foo.bar:,前提是 bar 在解释为布尔值时始终为真 — 如果 bar 可以是 0、[]{} 或任何其他具有假真值,你需要is not None。如果您正在测试可选部分(而不是在 TrueFalse 之间进行测试),这也更清楚。

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

  • 对象没有bar字段;
  • 该对象有一个 bar 字段,表示您认为的意思。

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

【讨论】:

  • Gilles 我可以问一个与问题本身无关但更多与 SO 哲学相关的附带问题吗?为什么这个问题被标记为非建设性的?它实际上是非常有建设性的(至少对我来说),因为它有 2 个很好的答案,比如你的和乔纳森的。谢谢
  • @Matteo Beats me(关于 SO 哲学,在这种情况下正确的做法是询问元数据)。
  • 最后,在编写了十多年的 Python 代码之后,我找到了使用 EAFP 的一个很好的理由——事实上,有两个!谢谢!我很惊讶也很伤心,我没有在 python.org 上读到这个推理。不幸的是,python.org 在阻止人们编写一般的except 子句方面也做得很差,结果我不得不处理许多难以发现的错误。无论如何,我认为这是最好的答案。
【解决方案3】:

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

通常请求宽恕而不是许可可以提高性能。

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

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

【讨论】:

  • 如果失败率足够高,会不会出现请求许可是首选选项的情况?在大多数情况下(和大多数语言),必须捕获异常比执行检查要慢很多倍,所以如果异常情况相对常见并且权限测试很快,那么肯定会更好地请求许可,对吧?
  • 确定测试不是免费的,异常处理也不是免费的。与许多其他语言相比,异常处理成本更低。如果您有一个快速测试并且测试失败的实例比例非常高,那么您可能会发现使用该测试会更快。在这种情况下,使用 python timeit 模块可以帮助您比较不同的方法,以确定哪种方法最优化。
  • 还可以查看所有其他 cmets,了解使用异常处理来避免竞争条件的功能优势,而不仅仅是性能。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    我个人的非宗教观点是,上述口头禅主要适用于记录很好理解的退出条件和边缘情况(例如 I/O 错误),并且永远不应该被用作马虎编程的出狱卡。

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

    # 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 并让生成的错误成为您识别和修复草率的指南代码。

    【讨论】:

      【解决方案6】:

      在 Python 上下文中,“请求宽恕而不是许可”意味着一种编程风格,在这种风格中,您不会事先检查事情是否符合您的预期,而是处理如果错误导致的错误。经典示例不是检查字典是否包含给定键,如下所示:

      d = {}
      k = "k"
      if k in d.keys():
        print d[k]
      else:
        print "key \"" + k + "\" missing"
      

      而是在密钥丢失时处理产生的错误:

      d = {}
      k = "k"
      try:
        print d[k]
      except KeyError:
        print "key \"" + k + "\" missing"
      

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

      【讨论】:

        【解决方案7】:

        虽然已经有许多高质量的答案,但主要是从风格的角度来讨论这个问题,与功能性的观点相对应。

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

        一个典型的例子是,

        if file_exists: 
            open_it()
        

        在此示例中,文件可能在检查和尝试实际打开文件之间已被删除。使用try 可以避免这种情况:

        try:
            open_it()
        except FileNotFoundException:
            pass # file doesn't exist 
        

        这出现在很多地方,通常与文件系统或外部 API 一起使用。

        【讨论】:

        • 这一点很重要。不要将请求宽恕 (AFF) 视为风格问题。在许多情况下,请求许可 (AFP) 在功能上是错误的,就像上面@nick 所说的竞争条件一样。换一种说法:在某些情况下,如果您使用 AFP,无论如何您最终都必须处理异常,因为当您执行下一行时,您检查的前提条件可能仍然不成立。无论如何,您必须处理异常,因此您不妨首先使用 AFF。并且不要让自己疯狂地分析是否存在竞争条件,只需使用 AFF。
        【解决方案8】:

        请求宽恕而不是许可旨在简化代码。当有合理的预期 .bar 可能会触发 AttributeError 时,它的意思是这样编写代码。

         try:
             print foo.bar
         except AttributeError as e
             print "No Foo!" 
        

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

        问题是,如果您有理由预期某事会失败,请使用 try/catch。如果您不期望某些事情会失败,并且无论如何都会失败,那么抛出的异常就相当于其他语言中的失败断言。您可以查看发生意外异常的位置并相应地调整您的代码/假设。

        【讨论】:

        • bare except 语句通常被认为是不好的做法。只捕获您知道如何处理的异常,仅此而已。
        • @mgilson 我同意,我只是在复制粘贴 OPs 代码。为AttributeError添加了一个捕获
        • 在我看来,比简化代码更重要的是 AFNP 避免了请求许可中固有的竞争条件。也就是说,在请求许可时,条件可能会在检查之后但在执行操作之前从“将成功”变为“不会成功”。虽然这在每个用例中都不是问题,但询问权限应该为任何审查代码的人提出一个危险信号,并且可以通过使 AFNP 成为一致的做法来避免这个危险信号。 @Gilles 对此的回答非常详细。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-06-11
        • 1970-01-01
        • 2018-07-27
        • 1970-01-01
        • 1970-01-01
        • 2011-01-11
        • 2018-11-05
        相关资源
        最近更新 更多