检查优先与异常处理?


88

我正在阅读“ Head First Python”一书(这是我今年要学习的语言),然后进入一节,他们讨论了两种代码技术:
检查First与Exception处理。

这是Python代码的示例:

# Checking First
for eachLine in open("../../data/sketch.txt"):
    if eachLine.find(":") != -1:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

# Exception handling        
for eachLine in open("../../data/sketch.txt"):
    try:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    except:
        pass

第一个示例直接处理.split函数中的问题。第二个只是让异常处理程序处理它(并忽略该问题)。

他们在书中主张使用异常处理而不是先检查。该论点是异常代码将捕获所有错误,其中首先进行检查将仅捕获您所考虑的事情(并且您错过了极端情况)。我被教导首先要检查,所以我的本能是这样做,但是他们的想法很有趣。我从未想过使用异常处理来处理案例。

通常认为这两种方法中的哪一种更好?


12
书中的那部分并不聪明。如果您处于循环中,并且会因异常昂贵而引发异常。我试图概述何时进行此操作的一些优点。
杰森·塞布林2012年

9
只是不要陷入“文件存在检查”陷阱。文件存在!=有权访问文件,或者它会在10毫秒后才
Billy ONeal 2012年

11
在Python和其他语言中,对异常的看法有所不同。例如,遍历集合的方法是在其上调用.next()直到引发异常。
WuHoUnited 2012年

4
@ emeraldcode.com关于Python并非完全如此。我不知道具体细节,但是该语言是围绕该范例构建的,因此抛出异常的费用几乎不比其他语言昂贵。
Izkata 2012年

就是说,对于这个示例,我将使用一个警卫声明: if -1 == eachLine.find(":"): continue,那么循环的其余部分也不会缩进。
Izkata 2012年

Answers:


68

在.NET中,通常的做法是避免过度使用异常。一个参数是性能:在.NET中,引发异常在计算上很昂贵。

避免过度使用它们的另一个原因是,读取太多依赖于它们的代码可能非常困难。Joel Spolsky的博客条目很好地描述了问题。

该论点的核心是以下引号:

原因是我认为例外并不比自1960年代以来就被认为有害的“ goto”更好,因为它们会导致从一个代码点到另一个代码点的突然跳跃。实际上,它们比goto的情况严重得多:

1.它们在源代码中不可见。查看代码块,包括可能会引发异常或可能不会引发异常的函数,因此无法查看可能会抛出异常以及从何处引发异常。这意味着即使仔细的代码检查也不会发现潜在的错误。

2.它们为功能创建了太多可能的出口点。要编写正确的代码,您实际上必须考虑函数中所有可能的代码路径。每次调用一个可能引发异常并且没有立即发现异常的函数时,都会为由突然终止的函数,导致数据处于不一致状态的函数或其他您没有遇到的代码路径引起的意外错误创造机会想一想。

就个人而言,当我的代码无法完成合同规定的操作时,我会抛出异常。当我要处理进程边界之外的内容时,例如SOAP调用,数据库调用,文件IO或系统调用,我倾向于使用try / catch。否则,我会尝试进行防御性编码。这不是一成不变的规则,但这是一种普遍做法。

Scott Hanselman还在此处撰写有关.NET中的异常的文章。在本文中,他描述了有关例外的几种经验法则。我的最爱?

您不应该为经常发生的事情抛出异常。然后他们将成为“普通人”。


5
还有一点:如果在整个应用程序范围内都启用了异常日志记录,则最好仅将异常用于特殊情况,而不是用于普通情况。否则,日志将变得混乱,真正的错误原因将被掩盖。
rwong 2012年

2
好答案。请注意,尽管例外在大多数平台上都有很高的性能。但是,正如您在我对其他答案的评论中所指出的那样,在为如何编纂某些事物确定总规则的情况下,性能不是一个考虑因素。
mattnz'3

1
Scott Hanselman的引用更好地描述了.Net对异常的态度,而不是“过度使用”。经常提到性能,但是真正的论点是为什么您应该使用异常的原因—当普通条件导致异常时,它会使代码更难以理解和处理。至于Joel,第1点实际上是一个正数(不可见意味着代码显示了它的作用,而不是它没有的作用),而第2点则无关紧要(您已经处于不一致状态,或者应该没有例外) 。仍然为“无法完成要求执行的操作” +1。
jmoreno'3

5
尽管此答案对.Net来说不错,但它不是pythonic的,因此鉴于这是一个python问题,我看不到为什么Ivc的答案没有得到更多支持。
Mark Booth

2
@IanGoldby:不。实际上,将异常处理更好地描述为异常恢复。如果您无法从异常中恢复,那么您可能应该没有任何异常处理代码。如果方法A调用方法B并调用C,并且C抛出异常,则很可能A或B都应该恢复,而不是两者都恢复。如果Y要求其他人完成任务,则应避免“如果我不能X我就Y”的决定。如果您无法完成任务,剩下的就是清理和日志记录。.net中的清理应该是自动的,日志记录应该是集中的。
jmoreno,2016年

78

特别是在Python中,通常认为捕获异常是更好的做法。与“先行后跃”(LBYL)相比,它通常被称为“ 比许可更容易寻求宽恕”(EAFP)。在某些情况下,LBYL会给您带来细微的错误

但是,请注意裸露的except:语句以及除语句之外的内容,因为它们都可以掩盖错误-像这样会更好:

for eachLine in open("../../data/sketch.txt"):
    try:
        role, lineSpoken = eachLine.split(":",1)
    except ValueError:
        pass
    else:
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

8
作为.NET程序员,我对此感到畏缩。但是话又说回来,你们所做的一切都很奇怪。:)
Phil

当API在何种情况下引发哪些异常不一致,或者在同一异常类型下引发多种不同类型的故障时,这会异常令人沮丧(无意中的双关语)。
2015年

因此,您最终将使用相同的机制处理意外错误和预期的返回值。这与将0用作数字,一个错误的布尔值和一个无效的指针差不多一样,它们将以128 + SIGSEGV的退出代码退出您的进程,因为多么方便,您现在不需要其他东西。像小子!或脚趾鞋…

2
@yeoman什么时候抛出异常是一个不同的问题,这是关于使用try/ except而不是为“以下对象是否有可能抛出异常”设置条件,Python的实践肯定更喜欢前者。这在这里(可能)更有效,这并不有害,因为在拆分成功的情况下,您只需遍历字符串一次。至于是否split在这里抛出异常,我肯定会说它应该-一个普遍的规则是,当您不能按照您的名字说的去做,并且不能在缺少的分隔符处拆分时,应该抛出该异常。
lvc

我没有发现它是坏的,缓慢的或可怕的,特别是当仅捕获到特定异常时。Ans我实际上很喜欢Python。有趣的是,有时它根本没有味道,正如C所说的那样使用数字零,Spork和Randall Munroe一直以来最喜欢的脚趾鞋:)另外,当我使用Python并且API表示这是这样做的方法,我会继续做的:)因为并发,协程,或者在以后添加的条件之一,所以预先检查条件当然绝不是一个好主意……
yeoman

27

务实的方法

您应该防守,但要注意一点。您应该编写异常处理,但是要讲一点。我将以网络编程为例,因为这是我的住所。

  1. 假设所有用户输入都是错误的,并且只能在数据类型验证,模式检查和恶意注入方面进行防御性写。防御性编程应该是您无法控制的可能经常发生的事情。
  2. 为有时可能失败的网络服务编写异常处理,并妥善处理以获取用户反馈。异常编程应用于可能不时失败但通常可靠的网络事物,并且需要保持程序正常运行。
  3. 验证输入数据后,不要费心在应用程序中进行防御性编写。这浪费时间,使您的应用程序膨胀。让它爆炸吧,因为它要么非常罕见,不值得处理,要么意味着您需要更仔细地看一下步骤1和2。
  4. 切勿在不依赖网络设备的核心代码中编写异常处理。这样做是不好的编程,并且会降低性能。例如,在循环中越界数组的情况下编写try-catch意味着您一开始就没有正确地编写循环。
  5. 让所有错误都由集中错误日志处理,该错误日志将按照上述步骤在一个地方捕获异常。您无法捕获所有可能无限的极端情况,只需要编写处理预期操作的代码即可。这就是为什么您将中央错误处理作为最后的手段。
  6. TDD很不错,因为它在某种程度上可以为您带来无限的尝试性捕获,这意味着您可以保证正常运行。
  7. 奖励点是使用代码覆盖率工具,例如Istanbul是一个很好的节点工具,因为它向您展示了您未测试的地方。
  8. 对所有这些的警告是对开发人员友好的例外。例如,如果您使用错误的语法并解释原因,则该语言将抛出。您的大部分代码所依赖的实用程序库也应该如此。

这是从在大型团队方案中工作的经验得出的。

比喻

想象一下,如果您一直都在ISS里面穿着太空服。根本很难去洗手间或吃饭。要在空间模块中四处移动,将会非常笨重。太烂了 在您的代码中编写一堆try-catching就是这样。您必须要说些什么,嘿,我确保了国际空间站的安全,而且我的宇航员都还可以,所以在每种可能的情况下都穿宇航服是不切实际的。


4
Point 3的问题在于它假定程序以及正在开发该程序的程序员都是完美的。它们不是,因此考虑到这些,最好是防御性的程序。关键时刻的适当数量可使软件比“如果检查输入一切都完美”的思想更加可靠。
mattnz'3

这就是测试的目的。
詹森·塞布林2012年

3
测试不是全部。我还没有看到具有100%代码和“环境”覆盖率的测试套件。
Marjan Venema 2012年

1
@emeraldcode:您想和我一起工作吗,我很乐意让团队中的某个人总是(除了例外)测试软件将要执行的每种情况的每个排列。一定要非常清楚地知道您的代码已经过完美测试。
mattnz 2012年

1
同意。在某些情况下,防御性编程和异常处理都可以正常工作,也有不好的情况,我们作为程序员,应该学会识别它们,并选择最适合的技术。我喜欢第3点,因为我认为我们需要在代码的特定级别上假设应该满足某些上下文条件。这些条件可以通过在代码的外层进行防御性编码来满足,并且我认为在这些假设在内层被打破时,异常处理是合适的。
yaobin '16

15

本书的主要论点是代码的异常版本更好,因为如果您尝试编写自己的错误检查,它将捕获您可能忽略的所有内容。

我认为这句话仅在非常特殊的情况下才是正确的-您不在乎输出是否正确。

毫无疑问,提出例外情况是一种安全可靠的做法。只要感觉到程序的当前状态中有某些您(作为开发人员)无法或不想处理的事情,就应该这样做。

但是,您的示例是关于捕获异常的。如果发现异常,就无法保护自己免受可能被忽略的情况的侵害。您所做的恰恰相反:您假设您没有忽略任何可能导致此类异常的情况,因此,您有把握捕获它(并防止它导致程序退出,就像任何未捕获的异常一样)。

使用异常方法,如果看到ValueError异常,则跳过一行。使用传统的非异常方法,您可以从中计算返回值的数量split,如果小于2,则跳过一行。您是否应该对异常方法感到更安全,因为您可能在传统的错误检查中忘记了其他一些“错误”情况,并except ValueError会为您抓住它们?

这取决于您的程序的性质。

例如,如果您正在编写Web浏览器或视频播放器,则输入问题不应导致它因未捕获的异常而崩溃。与退出相比,输出远为明智的内容(即使严格来说是错误的)要好得多。

如果您要编写对正确性很重要的应用程序(例如业务或工程软件),那么这将是一种糟糕的方法。如果您忘记了某些情况ValueError,那么最糟糕的事情就是忽略此未知情况,而直接跳过此行。这就是软件中最终出现的细微而昂贵的错误的原因。

您可能会认为,ValueError在此代码中看到的唯一方法是split仅返回一个值(而不是两个)。但是,如果您的print语句以后开始使用ValueError在某些条件下引发的表达式怎么办?这将导致您跳过某些行,不是因为它们错过了:,而是因为print它们失败了。这是我之前提到的一个细微错误的示例-您不会注意到任何东西,只会丢失一些行。

我的建议是避免在代码中捕获(但不要引发!)异常,在这些异常中产生不正确的输出比退出更糟。在此类代码中唯一一次捕获异常的时间是当我拥有一个真正的琐碎表达式时,因此我可以轻松推断出可能导致每种可能的异常类型的原因。

至于使用异常对性能的影响,这是微不足道的(在Python中),除非经常遇到异常。

如果确实使用异常来处理例行情况,则在某些情况下可能会付出巨大的性能成本。例如,假设您远程执行一些命令。您可以检查您的命令文本是否至少通过了最小验证(例如语法)。或者,您可以等待引发异常(仅在远程服务器解析您的命令并发现问题之后才发生)。显然,前者要快几个数量级。另一个简单的示例:您可以检查数字是否比尝试执行除法的速度快零〜10倍,然后捕获ZeroDivisionError异常。

仅当您经常将格式错误的命令字符串发送到远程服务器或接收用于除法的零值参数时,这些注意事项才有意义。

注意:我假设您会使用except ValueError而不是just except; 正如其他人指出的那样,正如本书本身在几页中所说的那样,您绝对不要使用裸露的内容except

另一个注意事项:正确的非异常方法是计算返回的值的数量split,而不是搜索:。后者太慢了,因为它重复了完成的工作,split并且可能使执行时间增加近一倍。


6

通常,如果您知道一条语句可能产生无效的结果,请对此进行测试并加以处理。对您不期望的事情使用异常;“例外”的东西。从合同的意义上讲,它使代码更清晰(例如,“不应为空”)。


2

使用一切正常的方法。

  • 您在代码可读性和效率方面选择的编程语言
  • 您的团队和一组约定的代码约定

异常处理和防御性编程都是表达同一意图的不同方法。


0

TBH,无论您使用的是try/except机制还是if语句检查都没关系。您通常会在大多数Python基准中同时看到EAFP和LBYL,而EAFP则更为常见。有时EAFP 更可读/地道,但在这种特殊情况下,我认为它的罚款无论哪种方式。

然而...

使用您当前的参考资料时,我会格外小心。他们的代码有两个明显的问题:

  1. 文件描述符泄漏。实际上,现代版本的CPython(特定的 Python解释器)会关闭它,因为它是一个匿名对象,仅在循环期间处于作用域内(gc会在循环后对其进行核对)。但是,其他口译员没有此保证。它们可能会直接泄漏描述符。with在Python中读取文件时,几乎总是要使用成语:几乎没有例外。这不是其中之一。
  2. 口袋妖怪异常处理掩盖错误(例如except,未捕获特定异常的裸语句)时,人们对此并不满意。
  3. Nit:元组拆包不需要parens。可以做role, lineSpoken = eachLine.split(":",1)

Ivc对此和EAFP都有很好的答案,但是也泄漏了描述符。

LBYL版本不一定具有与EAFP版本一样的性能,因此说抛出异常“在性能方面很昂贵”绝对是错误的。这实际上取决于您正在处理的字符串类型:

In [33]: def lbyl(lines):
    ...:     for line in lines:
    ...:         if line.find(":") != -1:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = line.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:

In [34]: def eafp(lines):
    ...:     for line in lines:
    ...:         try:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = eachLine.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:         except:
    ...:             pass
    ...:

In [35]: lines = ["abc:def", "onetwothree", "xyz:hij"]

In [36]: %timeit lbyl(lines)
100000 loops, best of 3: 1.96 µs per loop

In [37]: %timeit eafp(lines)
100000 loops, best of 3: 4.02 µs per loop

In [38]: lines = ["a"*100000 + ":" + "b", "onetwothree", "abconetwothree"*100]

In [39]: %timeit lbyl(lines)
10000 loops, best of 3: 119 µs per loop

In [40]: %timeit eafp(lines)
100000 loops, best of 3: 4.2 µs per loop

-4

基本上,异常处理应该更适合OOP语言。

第二点是性能,因为您不必eachLine.find每次都执行。


7
-1:性能是制定笼统规则的极差理由。
mattnz'3

3
不,异常与OOP完全无关。
Pubby 2012年

-6

我认为防御性编程会损害性能。您还应该仅捕获要处理的异常,让运行时处理您不知道如何处理的异常。


7
然而anotehr -1却担心性能会超过可读性,可维护性。性能不是原因。
mattnz'3

我能知道您为什么不加解释地分配-1s吗?防御性编程意味着更多的代码行,这意味着更差的性能。有人在降低分数之前愿意解释吗?
Manoj 2012年

3
@Manoj:除非您使用探查器进行了测量,发现一段代码的速度慢得令人无法接受,否则代码的可读性和可维护性要远远超出性能。
丹妮丝2012年

@Manoj所说的话,加上更少的代码通常意味着调试和维护时需要做的工作更少。除了完美的代码之外,任何东西都给开发人员带来了极大的损失。我假设(像我一样)您没有编写完美的代码,如果我错了,请原谅我。
mattnz'3

2
感谢您提供的链接-有趣的是,我必须同意这一点……在生命攸关的系统上工作,就像我所做的那样:“系统打印了堆栈跟踪,因此我们确切地知道为什么这300人不必要地死亡。 ....“在证人席上的位置下降得不太好。我想这是其中每种情况都有不同的适当响应的事情之一。
mattnz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.