前言这是“Python工匠”系列的第六篇文章。(点击原文链接查看该系列其他文章)如果你用Python编程,那么你就无法避免异常,因为异常在这门语言中无处不在。例如,当您在脚本执行过程中按ctrl+c退出时,解释器将产生KeyboardInterrupt异常。而KeyError、ValueError、TypeError等都是日常编程中随处可见的老朋友了。异常处理由“catch”和“throw”两部分组成。“捕捉”是指用try...except包裹特定的语句来正确完成错误处理。正确使用raise主动“抛出”异常是优雅代码的重要组成部分。在这篇文章中,我将分享3个与异常处理相关的好习惯。在继续阅读之前,希望你已经了解了以下知识点:异常的基本语法和用法(推荐阅读官方文档《错误与异常》)为什么要使用异常而不是错误返回(推荐阅读《让函数返回结果的技巧》)为什么写Python鼓励使用异常(推荐阅读《WriteCleanerPython:UseExceptions》)。三个好习惯1.只做最准确的异常捕获如果对异常机制了解不够,难免会对它产生一种天然的恐惧。你可能会想:异常是一件坏事,一个好的程序应该捕获所有的异常,让一切顺利运行。而用这种思路写的代码,通常会包含一大段模棱两可的异常捕获逻辑。让我们以一个可执行脚本为例:脚本中的save_website_title函数做了几件事。它首先通过网络获取网页内容,然后使用正则表达式匹配标题,最后将标题写入本地文件。而这里有两个容易出错的步骤:网络请求和本地文件操作。所以在代码中,我们使用一个很大的try...except语句块来结束这些步骤。安全第一。那么,这段看似简单易懂的代码到底隐藏着什么问题呢?如果你身边正好有一台安装了Python的电脑,你可以尝试运行上面的脚本。你会发现上面的代码无法执行成功。而且你还会发现,无论你怎么修改url和目标文件的值,程序还是会报错“savefailed:unableto...”。为什么?问题隐藏在这个巨大的try...except语句块中。如果您的眼睛靠近屏幕,请非常仔细地检查这段代码。你会发现我在写函数的时候犯了一个小错误。我把获取正则匹配字符串的方法打错为obj.group(1),少了一个'u'(obj.group(1))。但正是因为异常捕获过大和模糊,吞噬了本该因方法名错误而抛出的AttributeError。这给我们的调试过程增加了不必要的麻烦。异常捕获的目的不是捕获尽可能多的异常。如果我们从一开始就坚持:只做最准确的异常捕获。那么这样的问题就完全不会发生了。准确捕获包括:总是只捕获那些可能抛出异常的语句块尽量只捕获精确的异常类型,而不是模糊的异常根据这个原则,我们的例子应该改成这样:2.不要让异常破坏抽象的一致性关于四或者五年前,我正在为一个移动应用程序开发后端API项目。如果你有开发过后端API的经验,那么你一定知道,这样的系统需要制定一套“API错误码规范”,方便客户端处理调用错误。一个错误码返回大概是这样的:制定了错误码规范之后,接下来的任务就是如何实现。当时的项目使用的是Django框架,Django的错误页面是使用异常机制实现的。例如,如果您希望请求返回404状态码,只需在请求处理期间执行raiseHttp404即可。所以,我们很自然地从Django中汲取了灵感。首先,我们在项目中定义一个错误码异常类:APIErrorCode。然后根据《错误代码规范》,写了很多继承这个类的错误代码。当需要向用户返回错误信息时,只需加注一次即可完成。毫不奇怪,每个人都喜欢这种返回错误代码的方式。因为使用起来非常方便,不管调用栈有多深,只要你想给用户返回一个错误码,调用raiseerror_codes.ANY_THING就可以了。随着时间的推移,项目越来越大,抛出APIErrorCode的地方也越来越多。有一天,当我准备复用一个底层图像处理函数时,突然遇到了一个问题。看到一段代码让我很纠结:process_image函数会尝试解析一个文件对象,如果这个对象不能作为图片正常打开,就会抛出error_codes.INVALID_IMAGE_UPLOADED(APIErrorCode子类)异常,从而返回调用者JSON的错误代码。让我从头开始介绍这段代码。刚开始写process_image的时候,虽然放在了util.image模块中,但是当时调用这个函数的地方只有“处理用户上传图片的POST请求”。为了偷懒,我让函数直接抛出一个APIErrorCode异常来完成错误处理工作。说说当时的问题吧。当时需要写一个后台运行的批量图片脚本,正好复用了process_image函数实现的功能。但是这个时候不对劲。如果我想重用这个函数,那么:我必须捕获一个名为INVALID_IMAGE_UPLOADED的异常,即使我的图片根本没有被用户上传。我必须引入APIErrorCode异常类作为依赖来捕获即使我的脚本与DjangoAPI无关,这是异常类抽象级别不一致的结果。APIErrorCode异常类的意义在于表达了一个终端用户(人)可以直接识别和消费的“错误码”。它是整个项目中最高级别的抽象之一。但是为了方便,我们在底层模块中引入并抛出。这打破了image.processor模块的抽象一致性,影响了它的复用性和可维护性。这种情况属于“模块抛出高于其所属抽象层次的异常”。要避免此类错误,需要注意以下几点:让模块只抛出与当前抽象级别一致的异常。比如image.processer模块应该抛出自己封装的ImageOpenError异常,必要时进行异常封装和转换。比如应该在接近高层抽象的地方(视图View函数),将图像处理模块的ImageOpenError低层异常包转换成APIErrorCode高层异常修改代码:除了避免抛出异常高于当前抽象层级,我们也应该避免在抽象层级泄漏低于当前层级的异常。如果你使用过requests模块,你可能会发现它在请求页面错误时抛出的异常并不是其底层使用的urllib3模块的原始异常,而是通过requests.exceptions封装一次的异常。这样做也是为了保证异常类的抽象一致性。因为urllib3模块是requests模块依赖的一个底层实现细节,这个细节在以后的版本中可能会发生变化。因此,必须对它抛出的异常进行适当的封装,以防止日后底层的变化影响到请求客户端的错误处理逻辑。3.异常处理不应压倒主机。前面我们提到异常捕获必须精确,抽象层次必须一致。但在现实世界中,如果严格按照这些流程进行,很可能会遇到另一个问题:异常处理逻辑过多,打乱了代码的核心逻辑。具体表现为代码中充斥着大量的try、except、raise语句,导致核心逻辑难以识别。让我们看一个例子:这是一个处理用户上传头像的视图函数。这个函数里做了三件事情,每件事情都做了异常捕获。如果在做某事时发生异常,则向前端返回一个用户友好的错误。这样的处理流程虽然合理,但是代码中的异常处理逻辑显然有点“吃力”。乍一看,代码都是缩进的,很难提炼出代码的核心逻辑。早在2.5版本,Python语言就提供了应对此类场景的工具:“上下文管理器”。上下文管理器是一个特殊的Python对象,与with语句一起使用,使异常处理更容易。那么,我们如何使用上下文管理器来改进我们的异常处理过程呢?让我们直接上代码。在上面的代码中,我们定义了一个名为raise_api_error的上下文管理器,它在进入上下文时什么都不做。但是在退出上下文的时候会判断当前上下文中是否抛出了self.captures类型的异常,如果抛出则替换为APIErrorCode异常类。使用这个上下文管理器后,整个功能可以变得更加清晰和简洁:提示:推荐阅读PEP343--The"with"Statement|Python.org以了解有关上下文管理器的更多信息。模块contextlib还提供了许多与编写上下文管理器相关的实用函数和示例。总结在本文中,我分享了与异常处理相关的三个建议。最后总结一下要点:只捕获可能抛出异常的语句,避免捕获逻辑有歧义,保持模块异常类的抽象一致性,必要时对底层异常类进行包装使用“上下文管理器”简化重复异常处理看完文章顺理成章,你有什么可吐槽的吗?请发表评论或在项目GithubIssues中让我知道。附录题图来源:PhotobyBernardHermantonUnsplash更多系列文章地址:https://github.com/piglei/one...系列其他文章:Python工匠:让函数返回结果的技巧蓝鲸智云本文由腾讯蓝鲸智云编辑发布,腾讯蓝鲸智云(简称蓝鲸)软件系统是一套基于PaaS的技术解决方案,致力于打造行业领先的一站式自动化运维平台。目前已经推出社区版和企业版,欢迎体验。官网:https://bk.tencent.com/下载链接:https://bk.tencent.com/download/社区:https://bk.tencent.com/s-mart...
