Python 错误处理的终极指南(下)

简介: Python 错误处理的终极指南(下)

引言

我经常遇到一些开发者,他们对Python的错误处理机制了如指掌,但当我查看他们的代码时,却发现代码质量远远不够。Python的异常处理就是这样一个领域,它有一个广为人知的表层,以及一个更深层次、几乎不为人知的层面,许多开发者甚至没有意识到它的存在。如果你想测试一下自己对这个话题的理解,试着回答以下问题:

  • 你何时应该捕获你调用的函数引发的异常,何时又不应该?
  • 你如何确定应该捕获哪些异常类?
  • 当你捕获到一个异常时,你应该如何处理它?
  • 为什么说捕获所有异常是一种不好的做法,又在什么情况下这样做是可以接受的?

你准备好探索本文Python中错误处理的奥秘了吗?

捕获所有异常

你可能怀疑为什么类型4错误应该是你的应用程序中最常见的错误之一,因为如果让异常自由地冒泡,它们可能会一直冒泡到最顶层而没有在其他地方被捕获,导致应用程序崩溃。这是一个合理的担忧,但有一个简单的解决方案。

你应该设计你的应用程序,使其不可能让异常到达Python层。你可以通过在最顶层添加一个try/except块来捕获那些逃逸的异常。

如果你正在编写一个命令行应用程序,你可以这样做:

import sys

def my_cli()
    # ...

if __name__ == '__main__':
    try:
        my_cli()
    except Exception as error:
        print(f"Unexpected error: {error}")
        sys.exit(1)

在这个应用程序中,最顶层是在 if name == 'main' 条件判断中,它把所有到达这个级别的错误都视为可以恢复的。恢复的方式是向用户展示错误,然后以退出码1退出应用程序,这样会通知命令行或父进程应用程序已经失败。有了这样的逻辑,应用程序就知道如何以失败的方式退出,因此不需要在其他地方重新实现这一逻辑。应用程序可以简单地让错误继续冒泡,最终在这里被捕获,错误消息会被展示出来,然后应用程序会以错误代码退出。

你可能还记得我之前提到过,捕获所有异常是一种不好的做法。然而,这里正是我所做的!原因是在这个级别我们确实不能让任何异常到达Python层面,因为我们不希望这个程序崩溃,所以这是唯一一个捕获所有异常有意义的情况。这是一个例外,证明了规则。

拥有一个高层次的捕获所有异常的代码块实际上是大多数应用程序框架采用的一个常见模式。这里有两个例子:

  • Flask Web框架:Flask将每个请求视为应用程序的独立运行,其中 full_dispatch_request() 方法是最外层。捕获所有异常的代码就在这里。 Tkinter
  • GUI工具包(Python标准库的一部分):Tkinter将每个应用程序事件处理程序视为应用程序的独立小运行,并在每次调用处理程序时添加一个通用的捕获所有异常的代码块,以防止有缺陷的应用程序处理程序导致GUI崩溃。在这里查看代码。在这个代码片段中,注意Tkinter允许SystemExit异常(表示应用程序正在退出)继续冒泡,但捕获了所有其他异常以防止崩溃。

一个例子

我想向你展示一个例子,说明当你采用智能错误处理设计时如何改进你的代码。为此,我将使用Flask,但这同样适用于大多数其他框架或应用程序类型。

假设这是一个使用Flask-SQLAlchemy扩展的数据库应用程序。在我的咨询和代码审查工作中,我看到许多开发者在Flask端点中以如下方式编写数据库操作:

# NOTE: this is an example of how NOT to do exception handling!
@app.route('/songs/<id>', methods=['PUT'])
def update_song(id):
    # ...
    try:
        db.session.add(song)
        db.session.commit()
    except SQLAlchemyError:
        current_app.logger.error('failed to update song %s, %s', song.name, e)
        try:
            db.session.rollback()
        except SQLAlchemyError as e:
            current_app.logger.error('error rolling back failed update song, %s', e)
        return 'Internal Service Error', 500
    return '', 204

在这个路由中,它尝试将一首歌曲保存到数据库,并捕获所有继承自SQLAlchemyError异常类数据库错误。如果发生错误,它会将一条解释性信息记录到日志中,然后尝试回滚数据库会话。但很显然,回滚操作本身有时也会失败,因此还有一个额外的异常捕获块来处理回滚过程中可能出现的错误,并将它们也记录下来。经过这一连串操作后,向用户返回一个500错误代码,告知用户发生了服务器错误。这种模式在所有写入数据库的端点中反复出现。

这是一个非常糟糕的做法。首先,对于回滚错误,这个函数没有任何恢复的办法。如果发生了回滚错误,那意味着数据库遇到了严重的问题,你可能会持续遇到错误,记录一个回滚错误发生了对你没有任何帮助。其次,提交失败时记录错误信息乍一看似乎有用,但这个特定的日志缺少关键信息,尤其是错误堆栈跟踪,这在你之后调试时是最重要的工具。至少,这段代码应该使用logger.exception()而不是logger.error(),因为这样可以同时记录错误信息和堆栈跟踪。但我们完全可以做得更好。

由于这个端点属于类型4错误,可以采用“无为而治”的方法进行编码,从而得到一个更加优秀的实现:

@app.route('/songs/<id>', methods=['PUT'])
def update_song(id):
    # ...
    db.session.add(song)
    db.session.commit()
    return '', 204
  • 为什么这种方法有效?

正如你之前看到的,Flask会捕获所有错误,因此你的应用程序不会因为漏捕错误而崩溃。在其处理过程中,Flask会将错误消息和堆栈跟踪自动记录到Flask日志中,这正是我们所需要的,所以我们无需亲自动手。Flask还会向客户端返回一个500错误码,表示发生了意外的服务器错误。此外,Flask-SQLAlchemy扩展会自动集成到Flask的异常处理机制中,当数据库错误发生时,为你自动回滚会话,这是我们需要的最后一项重要功能。在路由中真的没有什么留给我们去做了!

数据库错误的恢复过程在大多数应用程序中是相同的,因此你应该让框架为你完成这些繁重的工作,而你则可以从自己应用程序代码中更简单的逻辑中获益。

生产环境与开发环境中的错误处理

我提到过,尽可能将错误处理逻辑移动到应用程序调用栈的更高层次有一个好处,那就是你的应用程序代码可以让这些错误自然冒泡而不必显式捕获它们,从而使代码更易于维护和阅读。

将大部分错误处理代码集中到应用程序的一个独立部分的另一个好处是,你可以更好地控制应用程序如何应对错误。最好的例子就是你可以多么容易地改变应用程序在生产环境和开发环境中的错误行为。

在开发过程中,应用程序崩溃并显示堆栈跟踪实际上并没有任何问题。实际上,这是一件好事,因为你希望错误和缺陷被注意到并被修复。但当然,相同的应用程序在生产环境中必须坚如磐石,错误被记录,如果可行的话,通知开发者,而不向最终用户泄露任何内部或私有的错误细节。

当错误处理逻辑集中且与应用程序逻辑分离时,这变得容易实现。让我们回到我前面分享的命令行示例,但现在让我们添加开发和生产模式:

import sys

mode = os.environ.get("APP_MODE", "production")

def my_cli()
    # ...

if __name__ == '__main__':
    try:
        my_cli()
    except Exception as error:
        if mode == "development":
            raise  # in dev mode we let the app crash!
        else:
            print(f"Unexpected error: {error}")
            sys.exit(1)

这是不是很棒?在开发模式下,我们现在重新抛出异常以导致应用程序崩溃,这样我们就可以在工作时看到错误和堆栈跟踪。但我们这样做的同时,并没有削弱生产版本的稳定性,它继续捕获所有错误并防止崩溃。更重要的是,应用程序逻辑不需要了解这些配置上的差异。

这是否让你想起了 Flask、Django 以及其他 Web 框架的某些特性?许多 Web 框架都有一个开发或调试模式,它们会在你的控制台甚至有时直接在 Web 浏览器中展示错误。这和我在一个假想的命令行应用程序中展示给你的解决方案是一样的,只不过应用到了 Web 应用程序上!

相关文章
|
8月前
|
Python
在Python错误处理基础讲解
在Python错误处理基础讲解
57 1
|
8月前
|
开发者 Python
Python中的异常处理:原理与实践
Python中的异常处理:原理与实践
118 0
|
3月前
|
存储 数据库 开发者
Python 错误处理的终极指南(上)
Python 错误处理的终极指南(上)
35 2
Python 错误处理的终极指南(上)
|
5月前
|
开发者 Python
Python中的异常处理机制及其实践
【8月更文挑战第12天】Python的异常处理机制通过`try`和`except`结构显著提高了程序的稳定性和可靠性。在`try`块中执行可能引发异常的代码,如果发生异常,控制权将转移到与该异常类型匹配的`except`块。此外,还可以通过`else`处理无异常的情况,以及使用`finally`确保某些代码无论如何都会被执行,非常适合进行清理工作。这种机制允许开发者精确地捕捉和管理异常,从而提升程序的健壮性和可维护性。同时,Python还支持定义自定义异常,进一步增强了错误处理的灵活性。
73 4
|
7月前
|
程序员 Python
Python进阶:错误和异常处理,你的代码还能更健壮吗?
【6月更文挑战第12天】Python编程中的错误和异常处理对确保代码健壮性至关重要。当遇到如文件未找到或除零运算等错误时,Python会抛出异常。通过try-except语句可以捕获并处理异常,例如处理ZeroDivisionError时,可以在except块中给出错误信息。此外,可使用else和finally块进行更精细的控制,以及通过继承Exception类定义自定义异常。掌握这些技巧能帮助编写出更稳定且能优雅处理异常的代码。
27 0
|
8月前
|
Python
Python 中的异常处理机制是一种强大的错误处理工具
【5月更文挑战第8天】Python的异常处理机制借助try/except结构管理错误,提高程序健壮性。异常是中断正常流程的问题,可由多种原因引发。基本结构包括try块(执行可能出错的代码)和except块(处理异常)。通过多个except块可捕获不同类型的异常,finally块确保无论是否异常都执行的代码。此外,raise语句用于主动抛出异常,自定义异常通过继承Exception类实现。with语句配合上下文管理器简化资源管理并确保异常情况下资源正确释放。
57 2
|
8月前
|
Python
python学习11-异常处理机制
python学习11-异常处理机制
|
8月前
|
Python
Python异常处理学习应用案例详解
在Python中,异常处理通过`try-except`语句确保程序稳定性。以下是一个示例:函数`divide(a, b)`执行除法运算,若除数`b`为0,则抛出`ZeroDivisionError`。通过异常处理,当发生此错误时,程序打印“除数不能为0!”而非崩溃。此外,Python还有`TypeError`、`ValueError`等其他内置异常类型,可针对不同异常编写特定处理逻辑,实现更全面的错误管理。
36 1
|
8月前
|
程序员 开发者 Python
Python中的异常处理技巧与最佳实践
异常处理在Python编程中至关重要,它能够有效地帮助开发人员识别和解决程序中的错误。本文将介绍Python中常见的异常类型,探讨异常处理的最佳实践,并提供一些实用的技巧,帮助开发者编写更健壮的代码。
|
8月前
|
运维 Shell Python
第七章 Python异常处理
第七章 Python异常处理