我们是否需要在每个函数中都使用try, except?

7
我们是否应该在编写的每个函数中都用 try...except 块包含起来? 我问这个问题是因为有时在一个函数中我们会抛出 异常,而调用该函数的调用方没有异常处理。
def caller():
   stdout, stderr = callee(....)


def callee():
   ....
    if stderr:
       raise StandardError(....)

如果我们的应用程序崩溃,那么在这种明显的情况下,我会考虑用 try..except 来包含被调用方和调用方。

但是,我已经阅读了很多Python代码,它们并不总是使用这些 try..block


def cmd(cmdl):
    try:
        pid = Popen(cmdl, stdout=PIPE, stderr=PIPE)
    except Exception, e:
        raise e
    stdout, stderr = pid.communicate()
    if pid.returncode != 0:
        raise StandardError(stderr)
    return (stdout, stderr)

def addandremove(*args,**kwargs):
    target = kwargs.get('local', os.getcwd())
    f = kwargs.get('file', None)
    vcs = kwargs.get('vcs', 'hg')

    if vcs is "hg":
        try:
            stdout, stderr = cmd(['hg', 'addremove', '--similarity 95'])
        except StandardError, e:
            // do some recovery
        except Exception, e:
            // do something meaningful
    return True

真正困扰我的是这个问题:

如果有第三个函数在其中一个语句中调用了addandremove(),我们是否也要用try..except块来包围这个调用?如果这第三个函数有3行代码,并且每个函数调用自己都有try-except块呢?抱歉我让问题变得复杂了。但这是我不理解的问题。


你应该只在你预期会出现错误的地方放置 try...except 块,并且只捕获你预期的特定类型的异常。 - Joel Cornett
6个回答

14

异常就像其名字所示,是用于特殊情况下的 - 本不应该发生的事情。

..因为它们可能不应该发生,大多数情况下,您可以忽略它们。这是一件好事。

有时候您确实需要处理某个特定的异常,例如如果我执行:

urllib2.urlopen("http://example.com")

在这种情况下,出现“无法联系服务器”错误是合理的,因此您可以这样做:
try:
    urllib2.urlopen("http://example.com")
except urllib2.URLError:
    # code to handle the error, maybe retry the server,
    # report the error in a helpful way to the user etc

然而,试图捕获每一个可能出错的错误是徒劳无功的——有太多的事情可能会出错。举个奇怪的例子,如果一个模块修改了urllib2并移除了urlopen属性,那么你不可能期望出现NameError,也没有合理的方式可以处理这种错误,因此你只能让异常传播上去。
代码退出并打印回溯信息是一件好事——它让你轻松地看到问题的起源和原因(基于异常及其消息),并修复问题的原因或在正确的位置处理异常……
简而言之,仅在可以对异常采取有用的操作时才处理异常。否则,试图处理所有可能的错误只会使你的代码变得更加错误和难以修复。
在你提供的示例中,try/except块什么都没做——它们只是重新引发异常,因此与以下更整洁的代码相同:
def cmd(cmdl):
    pid = Popen(cmdl, stdout=PIPE, stderr=PIPE)
    stdout, stderr = pid.communicate()
    if pid.returncode != 0:
        raise StandardError(stderr)
    return (stdout, stderr)

# Note: Better to use actual args instead of * and **,
# gives better error handling and docs from help()
def addandremove(fname, local = None, vcs = 'hg'):
    if target is None:
       target = os.getcwd() 

    if vcs is "hg":
        stdout, stderr = cmd(['hg', 'addremove', '--similarity 95'])
    return True

关于异常处理,我唯一可能期望的是处理如果找不到'hg'命令时产生的异常,结果并不特别描述。因此对于一个库,我会做如下处理:

class CommandNotFound(Exception): pass

def cmd(cmdl):
    try:
        pid = Popen(cmdl, stdout=PIPE, stderr=PIPE)
    except OSError, e:
        if e.errno == 2:
            raise CommandNotFound("The command %r could not be found" % cmdl)
        else:
            # Unexpected error-number in OSError,
            # so a bare "raise" statement will reraise the error
            raise

    stdout, stderr = pid.communicate()
    if pid.returncode != 0:
        raise StandardError(stderr)
    return (stdout, stderr)

这只是将可能令人困惑的“OSError”异常转换为更清晰的“CommandNotFound”异常。

重新阅读问题后,我怀疑您可能会误解Python异常的工作方式("并且调用此函数的调用者没有异常"这一部分),因此希望澄清:

调用方函数不需要了解可能从子函数中引发的异常。 您只需调用cmd()函数,并希望它能正常工作。

假设您的代码在一个mystuff模块中,其他人想要使用它,他们可以这样做:

import mystuff

mystuff.addandremove("myfile.txt")

或者,也许他们想要提供一个友好的错误信息并退出,如果用户没有安装 hg

import mystuff

try:
    mystuff.addandremove("myfile.txt")
except mystuff.CommandNotFound:
    print "You don't appear to have the 'hg' command installed"
    print "You can install it with by... etc..."
    myprogram.quit("blahblahblah")

很好的回答。(+1),不过根据你如何导入urllib2(使用import urllib2而不是from urllib2 import *),你可能会得到一个AttributeError而不是NameError :-p - mgilson
@mgilson 哎呀,困意 + REPL 中的打字错误让我过度纠正自己并写下了 NameError (import urllib; urllib2.blah),不过...这也证实了我的观点...我的意思是,我写那个来证实我的观点。是的... - dbr
EAFP原则使得异常不一定是“本不应该发生的事情”。 - Wooble
@dbr 谢谢!我采纳了所有的建议,这样做对吗?请看一下我的更新代码。 - CppLearner
@dbr 谢谢。所以“不重新引发”这部分对我来说很清楚。但最终的问题仍然不清楚。我在我的编辑中已经说明了。抱歉打扰你,但非常感谢你的帮助! - CppLearner
@CppLearner 如果 addandremove 函数处理了错误,调用该函数的函数就不需要关心 - 错误已经消失(被处理)。如果调用函数仍然需要关心(因为它无法继续执行),那么你只需让异常传播即可。我所描述的两个“内部”函数同样适用于 addandremove 和调用它的任何函数(正确完整地处理异常,或者让它传播)。 - dbr

6

try/except子句只有在您知道如何处理引发的错误时才真正有用。请看以下程序:

while True:
    n=raw_input("Input a number>")
    try:
       n=float(n)
       break
    except ValueError:
       print ("That wasn't a number!")  #Try again.

然而,你可能有这样一个函数:
def mult_2_numbers(x,y):
    return x*y

用户可能尝试将其用作:

my_new_list=mult_2_numbers([7,3],[8,7])

用户可以将这个内容放在try/except块中,但是接下来my_new_list将不被定义并且可能会在以后引发异常(通常是NameError)。在这种情况下,调试会变得更加困难,因为回溯中的行号/信息指向的是一个不是真正问题所在的代码部分。

没错!这就是问题所在。当我把主函数放在try..except里面时,我会被指向主函数异常的那一行,然后我就不知道发生了什么事情,可能要花一个小时来调试。但是,如果所有的10行都可能引起潜在的问题……你该怎么办呢?我的意思是,测试可以消除很多错误,但对于Web应用程序来说,一个函数可能会调用10个不同的函数来构建响应对象。但是当它返回时,其中一个属性为空,而主函数无法使用这个不完整的响应对象。 - CppLearner
@CppLearner -- 那就取决于程序员了。如果在接收到不完整的响应时有合理的操作可执行,那么使用try/except是有意义的(例如:尝试重新获取响应或跳过此响应等)。当然,如果您能够控制响应出错的原因,您可能希望在尝试再次获取之前至少打印一个警告,以便您知道存在问题... 如果没有办法从此错误中恢复,最好不要使用try/except,这样您就可以追踪到首次出现错误响应的原因。 - mgilson
1
谢谢你的建议。你介意看一下更新后的代码吗?我还是做错了什么吗? - CppLearner
1
@CppLearner -- 做 try: something(); except Exception as e: raise e 没有什么意义。你捕获了异常并重新引发它。最终,调用者看到的是相同的结果。如果你捕获了异常,你应该做些什么。你可以对异常进行处理(例如 print (e)),或者你可以采取其他行动--例如尝试再次运行命令或打印命令格式不正确,打印字符串并退出等。但是,如果你捕获了异常,你应该做更多的事情,而不仅仅是重新引发它。 - mgilson
@CppLearner - 如果addandremove无法完成其任务,则应引发异常。它只应在调用的函数/方法中捕获它知道如何处理这些异常的异常。如果它不知道如何处理异常,它应该让它落到堆栈中的下一个例程中。 - mgilson
显示剩余2条评论

6

您应该使用try catch块,以便可以具体定位异常的源头。您可以将这些块放置在任何想要的地方,但是除非它们产生某种有用的信息,否则没有必要添加它们。


1
+1. try...except 要放在你需要进行错误恢复的位置。如果你没有错误恢复,那么当出现问题时你的应用程序会崩溃。这是定义上的事实。 - James Youngman

1

对于您的编码团队来说,有几个关于内省类型工具和“异常”处理的编程决策需要做出。

在操作系统调用(如文件操作)中使用异常处理是一个很好的选择。原因是,例如,文件可能被访问限制,无法被客户端应用程序访问。这种访问限制通常是操作系统管理员的任务,而不是Python应用程序函数的任务。因此,在应用程序没有控制权的情况下,异常处理是一个很好的选择。

您可以将前面段落中异常的应用范围扩大,例如将其用于超出您的团队编写控制范围的所有“设备”或操作系统资源,如操作系统计时器、符号链接、网络连接等。

另一个常见的用例是当您使用一个旨在抛出大量异常的库或包,并且该包希望您捕获或编写相关代码时。一些包旨在尽可能少地抛出异常,并期望您基于返回值编写代码。然后,您的异常应该很少发生。

一些编码团队使用异常作为记录应用程序中“事件”的边缘情况的一种方式。

在决定是否使用异常时,我发现我要么编写以最小化失败为目标的程序,通过不使用大量的try/except并使调用例程期望有效返回值或无效返回值。要么我会编写针对失败的程序。也就是说,我会编写针对函数预期失败的程序,这时我会使用很多try/except块。将编程针对“预期”失败看作是与TCP一起工作;数据包不能保证到达或按顺序到达,但是可以通过使用发送/读取重试等TCP异常处理来解决。

个人而言,我会在最小可能的代码块周围使用try-except块,通常是一行代码。


0

由你决定;主要异常角色包括(从this精彩的书中引用):

  • 错误处理
  • 事件通知
  • 特殊情况处理
  • 终止操作
  • 异常控制流

0
当你知道错误会是什么时,可以使用try/except进行调试。否则,你不必为每个函数都使用try/except。

使用try/except如何帮助调试?未处理异常的回溯将为您提供通过处理它可以打印的所有信息... - mgilson
我认为使用中间的 try/catch 来添加链式异常形式的额外数据到跟踪信息可能是有意义的。因此,如果你不仅想知道问题的源位置还想知道相关的数据项,那么这可能很有用。如果这是回答的意图,那么提供更多详细信息的编辑将是受欢迎的。 - MvG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接