我正在阅读《 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函数中的问题。第二个只是让异常处理程序处理它(并忽略该问题)。

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

通常认为这两种方法中最好的做法是?

评论

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

只是不要陷入“文件存在检查”陷阱。文件存在!=有权访问文件,或者它将在10毫秒内存在,以进行我的文件打开呼叫,等等。blogs.msdn.com/b/jaredpar/archive/2009/04/27/…

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

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

就是说,对于这个示例,我将使用一条保护语句:如果-1 == eachLine.find(“:”):Continue,那么循环的其余部分也不会缩进。

#1 楼

在.NET中,避免过度使用异常是一种常见的做法。性能是一个参数:在.NET中,引发异常在计算上非常昂贵。
避免它们被过度使用的另一个原因是,读取太多依赖于它们的代码可能非常困难。乔尔·斯波斯基(Joel Spolsky)的博客文章很好地描述了问题。
论点的核心是以下引号:

原因是我认为例外并不比“转到”更好。 ”,自从1960年代以来就被认为是有害的,因为它们造成了从一个代码点到另一个代码点的突然跳跃。实际上,它们比goto的要差得多:
1。它们在源代码中不可见。查看代码块,包括可能会抛出异常或可能不会抛出异常的函数,无法查看可能会抛出异常以及从何处抛出异常。这意味着即使仔细检查代码也不会发现潜在的错误。
2。它们为一个函数创建了太多可能的出口点。要编写正确的代码,您实际上必须考虑函数中所有可能的代码路径。每次调用一个可能引发异常而没有立即发现异常的函数时,都会为由突然终止,使数据处于不一致状态的函数或其他您未曾遇到的代码路径引起的意外错误创造机会考虑一下。

我个人而言,当我的代码无法完成合同规定的操作时,我会抛出异常。当我要处理进程边界之外的内容时,例如SOAP调用,数据库调用,文件IO或系统调用,我倾向于使用try / catch。否则,我会尝试进行防御性编码。这不是一成不变的规则,但这是一种普遍做法。
Scott Hanselman还在此处撰写有关.NET中的异常的文章。在本文中,他描述了有关例外的几种经验法则。我最喜欢的吗?

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


评论


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

–rwong
2012年11月11日4:12

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

–mattnz
2012年11月11日4:42



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

– jmoreno
2012年11月11日17:14

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

– Mark Booth
2012年3月13日在13:57



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

– jmoreno
16 Sep 26'在10:37

#2 楼

特别是在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())


评论


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

–菲尔
2014年4月4日在19:24

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

– Yeoman
16年5月31日在7:55



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

–lvc
16年5月31日在8:44

我最喜欢这个答案,但是它仍然包含“未经允许的宽恕”的最大危险:假设只有一个潜在的异常原因。即使老实说我们不在乎输入文件中不包含冒号的任何行,ValueError还是一个相当广泛的异常,仅比Pokémon样式(“抓住所有东西”)异常处理稍好。在一个真实的示例中,很容易想象在try块中添加的代码可能会导致另一个ValueError,而该ValueError会被静默忽略,从而创建了一个难以发现的错误。

–迈克尔·谢珀(Michael Scheper)
16年11月23日在18:30



@Kevin:我绝对不是反例外,甚至不是反AFNP。我只是对AFNP应用不当造成的难以发现的bug感到厌倦,并且一直在努力阻止我的机灵但经验不足的开发人员避免编写此类bug。老实说,我还没有听说过多行尝试块是一种气味,而且我已经读了很多书,因此,如果我忽略了一个重要的参考文献,我想请您引用一下。无论如何,单行代码通常可以生成许多类的异常,因此Pokémon确实对我来说仍然很香。我使用过的每一种棉绒工具也会把它们嗅出来。

–迈克尔·谢珀(Michael Scheper)
18-10-29在14:32

#3 楼

务实的方法

你应该保持防御,但是要有一点。您应该编写异常处理,但要讲一点。我将以Web编程为例,因为这就是我的住所。


假定所有用户输入均不正确,并且只能在数据类型验证,模式检查方面进行防御性写和恶意注射。防御性编程应该是您无法控制的可能经常发生的事情。
为网络服务编写异常处理,该服务有时可能会失败,并可以妥善处理以获取用户反馈。异常编程应用于可能偶尔会失败但通常可靠的网络事物,并且您需要保持程序正常运行。
输入数据经过验证后,请不要在应用程序中进行防御性编写。这浪费时间,使您的应用程序膨胀。让它爆炸是因为它要么非常罕见,不值得处理,要么意味着您需要更仔细地看一下步骤1和步骤2。
永远不要在不依赖于网络的核心代码中编写异常处理设备。这样做是不好的编程,并且会降低性能。例如,在循环中越界数组的情况下编写try-catch意味着您一开始就没有正确地编写循环。
让所有错误都由中央错误日志处理,该日志将在一个地方捕获异常请按照上述步骤操作。您无法捕获所有可能无限的极端情况,只需要编写处理预期操作的代码即可。这就是为什么将中央错误处理作为最后的选择的原因。 TDD很好,因为在某种程度上可以为您进行尝试捕获而不会blo肿,这意味着可以保证正常运行。
要使用奖励积分一种代码覆盖率工具,例如Istanbul是一个很好的节点工具,因为它向您展示了您不在测试的地方。

所有这些的警告是对开发人员友好的例外。例如,如果您使用错误的语法并解释原因,则该语言将抛出。您的大部分代码都依赖于您的实用程序库。

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

类比

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

评论


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

–mattnz
2012年3月11日在7:34

这就是测试的目的。

–杰森·塞布林(Jason Sebring)
2012年11月11日15:59

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

– Marjan Venema
2012年3月11日在16:57

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

–mattnz
2012年11月11日19:12

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

–姚斌
16年2月19日在15:05

#4 楼

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

我认为该语句仅在非常特殊的情况-不在乎输出是否正确。

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

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

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

这取决于程序的性质。

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

如果您在编写对正确性很重要的应用程序(例如业务或工程软件),那么这将是一种糟糕的方法。如果您忘记了引发ValueError的某些情况,那么您最糟糕的事情就是静默忽略此未知情况,然后跳过该行。这就是软件中非常微妙且代价高昂的错误的原因。
您可能会认为,在此代码中看到ValueError的唯一方法是,如果split仅返回一个值(而不是两个)。但是,如果您的print语句稍后在某些情况下开始使用引发ValueError的表达式怎么办?这将导致您跳过某些行,不是因为它们错过了:,而是因为print在它们上面失败了。这是我之前提到的一个微妙错误的示例-您不会注意到任何东西,只会丢失一些行。

我的建议是避免在代码中捕获(但不要引发!)异常。产生错误的输出比退出更糟糕。在这样的代码中,我唯一一次捕获到异常的时候就是当我拥有一个微不足道的表达式时,因此我可以轻松地推断出可能导致每种可能的异常类型的原因。
使用异常,除非经常遇到异常,否则这是微不足道的(在Python中)。

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

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

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

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

#5 楼

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

#6 楼

在代码可读性和效率方面,请使用您选择的编程语言。



您的团队和一组公认的代码约定

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

#7 楼

TBH,无论您使用try/except机械师还是使用if语句检查都没有关系。您通常会在大多数Python基准中同时看到EAFP和LBYL,而EAFP则更为常见。有时EAFP更具可读性/习惯用法,但是在这种情况下,我认为这两种方法都很好。

但是...

我会谨慎使用您当前的参考。它们的代码有两个明显的问题:文件描述符泄漏。实际上,现代版本的CPython(特定的Python解释器)会关闭它,因为它是一个匿名对象,仅在循环期间处于作用域内(gc会在循环后对其进行核对)。但是,其他口译员没有此保证。它们可能会直接泄漏描述符。在Python中读取文件时,您几乎总是想使用with习惯用法:很少有例外。这不是其中之一。

宠物小精灵异常处理由于掩盖错误而倍受皱眉(即未捕获特定异常的裸except语句)
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


#8 楼

基本上,异常处理应该更适合OOP语言。第二点是性能,因为您不必对每一行都执行eachLine.find

评论


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

–mattnz
2012年3月11日4:39

不,异常与OOP完全无关。

– Pubby
2012年11月11日4:42

#9 楼

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

评论


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

–mattnz
2012年11月11日4:41

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

– Manoj
2012年3月11日在16:23



@Manoj:除非您使用探查器进行了测量,然后发现一段代码的速度慢得令人无法接受,否则可读性和可维护性的代码要远远优于性能。

–丹妮丝
2012年3月11日17:28

@Manoj补充说的是,更少的代码普遍意味着调试和维护时需要进行的工作更少。除了完美的代码之外,任何东西都给开发人员带来了极大的损失。我假设(像我一样)您没有编写完美的代码,如果我错了,请原谅我。

–mattnz
2012年3月11日19:08

感谢您提供的链接-有趣的是,我必须同意这一点……在生命攸关的系统上工作,就像我所做的那样:“系统打印了堆栈跟踪,因此我们确切地知道为什么这300人不必要地死亡。 ....“在证人席上的位置下降得不太好。我认为这是其中每种情况都有不同的适当响应的事情之一。

–mattnz
2012年12月12日下午2:30