我应该如何正确处理Python3中的异常

13 投票
2 回答
13493 浏览
提问于 2025-04-15 23:11

我不太明白在这里应该处理什么样的异常,什么样的异常应该重新抛出,或者干脆不处理,之后再在更高层次上处理。举个例子:我用Python3写了一个客户端/服务器应用,使用了SSL通信。客户端需要检查文件之间的差异,如果有差异,就应该把这个“更新过的”文件发送给服务器。


class BasicConnection:
    #blablabla
    def sendMessage(self, sock, url, port, fileToSend, buffSize):
        try:
            sock.connect((url, port))
            while True:
                data = fileToSend.read(buffSize)
                if not data: break
                sock.send(data)
            return True
        except socket.timeout as toErr:
            raise ConnectionError("TimeOutError trying to send File to remote socket: %s:%d"
                                  % (url,port)) from toErr
        except socket.error as sErr:
            raise ConnectionError("Error trying to send File to remote socket: %s:%d"
                                  % (url,port)) from sErr
        except ssl.SSLError as sslErr:
            raise ConnectionError("SSLError trying to send File to remote socket: %s:%d"
                                  % (url,port)) from sslErr
        finally:
            sock.close()

在Python中使用异常的方式对吗?问题是:如果file.read()抛出IOError,我应该在这里处理它,还是干脆不管,等到后面再处理?还有很多其他可能的异常呢?

  1. 客户端使用这个类(BasicConnection)来发送更新的文件到服务器:

class PClient():
    def __init__(self, DATA):
        '''DATA = { 'sendTo'      : {'host':'','port':''},
                    'use_ssl'     : {'use_ssl':'', 'fileKey':'', 'fileCert':'', 'fileCaCert':''},
                    'dirToCheck'  : '',
                    'localStorage': '',
                    'timeToCheck' : '',
                    'buffSize'    : '',
                    'logFile'     : ''}   '''
        self._DATA = DATA
        self._running = False
        self.configureLogging()


    def configureLogging(self):
        #blablabla

    def isRun(self):
        return self._running

    def initPClient(self):
        try:
            #blablabla

            return True
        except ConnectionError as conErr:
            self._mainLogger.exception(conErr)
            return False
        except FileCheckingError as fcErr:
            self._mainLogger.exception(fcErr)
            return False
        except IOError as ioErr:
            self._mainLogger.exception(ioErr)
            return False
        except OSError as osErr:
            self._mainLogger.exception(osErr)
            return False


    def startPClient(self):
        try:
            self._running = True
            while self.isRun():
                try :
                    self._mainLogger.debug("Checking differences")
                    diffFiles = FileChecker().checkDictionary(self._dict)

                    if len(diffFiles) != 0:
                        for fileName in diffFiles:
                            try:
                                self._mainLogger.info("Sending updated file: %s to remote socket: %s:%d"
                                    % (fileName,self._DATA['sendTo']['host'],self._DATA['sendTo']['port']))
                                fileToSend = io.open(fileName, "rb")
                                result = False
                                result = BasicConnection().sendMessage(self._sock, self._DATA['sendTo']['host'],
                                                                       self._DATA['sendTo']['port'], fileToSend, self._DATA['buffSize'])
                                if result:
                                    self._mainLogger.info("Updated file: %s was successfully delivered  to remote socket: %s:%d"
                                    % (fileName,self._DATA['sendTo']['host'],self._DATA['sendTo']['port']))
                            except ConnectionError as conErr:
                                self._mainLogger.exception(conErr)
                            except IOError as ioErr:
                                self._mainLogger.exception(ioErr)
                            except OSError as osErr:
                                self._mainLogger.exception(osErr)

                        self._mainLogger.debug("Updating localStorage %s from %s " %(self._DATA['localStorage'], self._DATA['dirToCheck']))
                        FileChecker().updateLocalStorage(self._DATA['dirToCheck'],
                                                         self._DATA['localStorage'])
                    self._mainLogger.info("Directory %s were checked" %(self._DATA['dirToCheck']))
                    time.sleep(self._DATA['timeToCheck'])
                except FileCheckingError as fcErr:
                    self._mainLogger.exception(fcErr)
                except IOError as ioErr:
                    self._mainLogger.exception(ioErr)
                except OSError as osErr:
                    self._mainLogger.exception(osErr)
        except KeyboardInterrupt:
            self._mainLogger.info("Shutting down...")
            self.stopPClient()
        except Exception as exc:
            self._mainLogger.exception(exc)
            self.stopPClient()
            raise RuntimeError("Something goes wrong...") from exc

    def stopPClient(self):
        self._running = False

这样做对吗?有没有人愿意花时间帮我理解一下在Python中处理异常的正确方式?我搞不清楚像NameError、TypeError、KeyError、ValueError这些异常该怎么处理……它们可能在任何语句、任何时候被抛出……如果我想记录所有的异常,该怎么做呢?

  1. 通常人们应该记录哪些信息?如果发生错误,我应该记录哪些信息?是记录所有的追踪信息,还是只记录相关的错误信息,或者其他什么呢?

  2. 希望有人能帮我。非常感谢。

2 个回答

3

首先,你不需要什么 _mainLogger。

如果你想捕捉到任何异常,比如记录日志或者发邮件之类的,最好在最上层处理这些异常——绝对不要在这个类里面处理。

另外,你也不应该把每个异常都转成 RuntimeError。让它自然出现。现在 stopClient() 方法没有什么用,等它有用的时候我们再来看看。

你可以把 ConnectionError、IOError 和 OSError 这几种异常一起处理(比如,把它们重新抛出成其他类型),但也就这么多了……

24

一般来说,你应该“捕捉”那些你预期会发生的错误(因为这些错误可能是用户操作不当,或者是程序无法控制的环境问题造成的),尤其是当你知道你的代码可以如何处理这些错误时。虽然在错误报告中提供更多细节是次要问题,但有些程序的规范可能要求这样做(例如,一个不应该因为这些问题而崩溃的长时间运行的服务器,而是应该记录大量状态信息,给用户一个总结性的解释,并继续处理后续请求)。

NameErrorTypeErrorKeyErrorValueErrorSyntaxErrorAttributeError等等,可以看作是程序内部的错误——也就是bug,而不是程序员无法控制的问题。如果你发布的是一个库或框架,让其他代码调用你的代码,那么这些bug很可能出现在其他代码中;通常情况下,你应该让异常继续传播,以帮助其他程序员调试他们自己的bug。如果你发布的是一个应用程序,那么这些bug就是你的责任,你必须选择一种策略来帮助你找到它们。

如果你的bug在最终用户运行程序时出现,你应该记录大量的状态信息,并给用户一个总结性的解释和道歉(如果你无法自动化这个过程,或至少在发送任何信息之前征得用户的同意)。你可能能够保存用户到目前为止的一些工作,但在一个已知存在bug的程序中,这通常也可能无法成功。

当然,大多数bug应该在你自己的测试中出现;在这种情况下,传播异常是有用的,因为你可以将其连接到调试器,深入探讨bug的细节。

有时候,一些异常的出现只是因为“请求原谅比请求许可更容易”(EAFP)——这是Python中一个完全可以接受的编程技巧。在这种情况下,你当然应该立即处理它们。例如:

try:
    return mylist[theindex]
except IndexError:
    return None

在这里,你可能期望theindex通常是mylist的有效索引,但偶尔会超出mylist的范围——而在这个假设的应用程序中,这种情况并不是错误,只是一个小的异常,可以通过将列表概念上扩展到两边无限数量的None来解决。尝试/异常处理比正确检查索引的正负值要简单得多(如果超出范围的情况确实很少发生,速度也更快)。

类似的,KeyErrorAttributeError的适用情况较少,得益于getattr内置函数和字典的get方法(允许你提供默认值),collections.defaultdict等;但列表没有这些的直接等价物,因此对于IndexError,尝试/异常处理的情况更为常见。

尝试捕捉语法错误、类型错误、值错误、名称错误等情况则相对少见且更具争议——不过如果错误是在“插件”或第三方代码中诊断出来的,而你的框架/应用程序试图动态加载和执行这些代码,那么这样做当然是合适的(实际上这是你提供库或类似内容时,需要与可能存在bug的外部代码和平共处的情况)。类型和值错误有时可能在EAFP模式中出现——例如,当你尝试重载一个函数以接受字符串或数字,并在每种情况下表现略有不同,捕捉这些错误可能比检查类型更好——但这种重载函数的概念往往是相当可疑的。

回到“用户和环境错误”,用户在输入时不可避免地会犯错误,比如指定一个实际上不存在的文件名(或者你没有权限读取或写入的文件名),等等:所有这些错误当然应该被捕捉,并给用户一个清晰的解释,说明出了什么问题,并给予他们重新输入的机会。网络有时会中断,数据库或其他外部服务器可能无法按预期响应,等等——有时捕捉这些问题并重试是值得的(也许在稍等片刻后——也许给用户一个关于出什么问题的提示,例如,他们可能不小心拔掉了电缆,你想给他们一个机会来修复问题,并告诉你何时重试),有时(尤其是在无人值守的长时间运行程序中)你能做的也只是有序关闭程序(并详细记录环境中每个可能相关的方面)。

所以,总的来说,你的问题标题的答案是,“这要看情况”;-)。我希望我列出了许多可能的情况和方面,并推荐了对这些问题通常最有用的态度。

撰写回答