• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

使用 Python 进行专业错误处理

在本教程中,您将学习如何从整个系统的角度处理 python 中的错误情况。错误处理是设计的一个关键方面,它从最低级别(有时是硬件)一直到最终用户。如果您没有一致的策略,您的系统将不可靠,用户体验会很差,并且您将面临很多调试和故障排除的挑战。

成功的关键是了解所有这些相互关联的方面,明确地考虑它们,并形成解决每个问题的解决方案。

状态码与异常

有两种主要的错误处理模型:状态码和异常。任何编程语言都可以使用状态码。异常需要语言/运行时支持。

Python 支持异常。Python 及其标准库大量使用异常来报告许多异常情况,例如 IO 错误、除以零、超出范围索引,以及一些不那么异常的情况,例如迭代结束(尽管它是隐藏的)。大多数库都效仿并引发异常。

这意味着您的代码无论如何都必须处理 Python 和库引发的异常,因此您最好在必要时从代码中引发异常,而不是依赖状态代码。

快速示例

在深入了解 Python 异常和错误处理最佳实践的内部圣地之前,让我们看看一些异常处理的实际应用:

def f():
    return 4 / 0
def g():
    raise Exception("Don't call us. We'll call you")
def h():
    try:
        f()
    except Exception as e:
        print(e)
     
    try:
        g()
    except Exception as e:
        print(e)

这是调用时的输出h():

h()
division by zero
Don't call us. We'll call you

Python 异常

Python 异常是按类层次结构组织的对象。

这是整个层次结构:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StandardError
      |    +-- BufferError
      |    +-- ArithmeticError
      |    |    +-- FloatingPointError
      |    |    +-- OverflowError
      |    |    +-- ZeroDivisionError
      |    +-- AssertionError
      |    +-- AttributeError
      |    +-- EnvironmentError
      |    |    +-- IOError
      |    |    +-- OSError
      |    |         +-- WindowsError (Windows)
      |    |         +-- VMSError (VMS)
      |    +-- EOFError
      |    +-- ImportError
      |    +-- LookupError
      |    |    +-- IndexError
      |    |    +-- KeyError
      |    +-- MemoryError
      |    +-- NameError
      |    |    +-- UnboundLocalError
      |    +-- ReferenceError
      |    +-- RuntimeError
      |    |    +-- NotImplementedError
      |    +-- SyntaxError
      |    |    +-- IndentationError
      |    |         +-- TabError
      |    +-- SystemError
      |    +-- TypeError
      |    +-- ValueError
      |         +-- UnicodeError
      |              +-- UnicodeDecodeError
      |              +-- UnicodeEncodeError
      |              +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
  +-- ImportWarning
  +-- UnicodeWarning
  +-- BytesWarning

有几个直接从 BaseException 派生的特殊异常,例如 SystemExit、KeyboardInterrupt 和 GeneratorExit。 然后是 Exception 类,它是 StopIteration、StandardError 和 Warning 的基类。 所有的标准误差都是从 StandardError 派生的。

当您引发异常或您调用的某个函数引发异常时,该正常代码流将终止并且异常开始在调用堆栈上传播,直到遇到适当的异常处理程序。如果没有可用的异常处理程序来处理它,则进程(或更准确地说是当前线程)将以未处理的异常消息终止。

引发异常

引发异常非常容易。您只需使用raise关键字来引发作为该类的子类的对象Exception。它可以是它Exception自己的一个实例、标准异常之一(例如RuntimeError),或者是Exception您自己派生的一个子类。这是一个演示所有情况的小片段:

# Raise an instance of the Exception class itself
raise Exception('Ummm... something is wrong')
 
# Raise an instance of the RuntimeError class
raise RuntimeError('Ummm... something is wrong')
 
# Raise a custom subclass of Exception that keeps the timestamp the exception was created
from datetime import datetime
 
class SuperError(Exception):
    def __init__(self, message):
        Exception.__init__(message)
        self.when = datetime.now()
 
raise SuperError('Ummm... something is wrong')

捕捉异常

except正如您在示例中看到的那样,您可以使用该子句捕获异常。考虑下面的例子:

while True:
    try:
        x = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

当您运行上面的代码块时,程序首先执行 try 子句之后的代码。如果没有发生异常,则程序跳过 except 子句。另一方面,如果发生错误,程序将执行 except 子句之后的语句。 

如果您输入一个整数,程序将按预期运行。但是,如果输入浮点数或字符串,程序将停止执行。

Please enter a number: 10.3
Oops!  That was no valid number.  Try again...
Please enter a number: hello
Oops!  That was no valid number.  Try again...
Please enter a number: 10.0
Oops!  That was no valid number.  Try again...
Please enter a number:

当您捕获异常时,您有三个选择:

  • 安静地吞下它(处理它并继续运行)。

  • 做一些类似记录的事情,但重新引发相同的异常以让更高级别处理。

  • 引发不同的异常而不是原始异常。

吞下例外

如果你知道如何处理它并且可以完全恢复,你应该吞下这个异常。

例如,如果您收到可能采用不同格式(JSON、YAML)的输入文件,您可以尝试使用不同的解析器对其进行解析。如果 JSON 解析器引发了该文件不是有效 JSON 文件的异常,则将其吞下并尝试使用 YAML 解析器。如果 YAML 解析器也失败了,那么你就让异常传播出去。

import json
import yaml
 
def parse_file(filename):
    try:
        return json.load(open(filename))
    except json.JSONDecodeError
        return yaml.load(open(filename))

请注意,其他异常(例如,找不到文件或没有读取权限)将传播出去,并且不会被特定的 except 子句捕获。在这种情况下,这是一个很好的策略,您希望仅在 JSON 解析由于 JSON 编码问题而失败时尝试 YAML 解析。

如果要处理所有异常,则只需使用except Exception. 例如:

def print_exception_type(func, *args, **kwargs):
    try:
        return func(*args, **kwargs)
    except Exception as e:
        print(type(e))

请注意,通过添加as e,您将异常对象绑定到e您的 except 子句中可用的名称。

重新引发相同的异常

要重新加注,只需raise在处理程序中添加不带参数的内容。这使您可以执行一些本地处理,但仍然让上层也处理它。在这里,该invoke_function()函数将异常类型打印到控制台,然后重新引发异常。

def invoke_function(func, *args, **kwargs):
    try:
        return func(*args, **kwargs)
    except Exception as e:
        print(type(e))
        raise

引发不同的异常

在几种情况下,您会想要引发不同的异常。有时您希望将多个不同的低级异常分组到一个类别中,由更高级别的代码统一处理。在订单情况下,您需要将异常转换为用户级别并提供一些特定于应用程序的上下文。

最后条款

有时您希望确保执行一些清理代码,即使在此过程中某个地方引发了异常。例如,您可能想要在完成后关闭一个数据库连接。这是错误的方法:

def fetch_some_data():
    db = open_db_connection()
    query(db)
    close_db_Connection(db)

如果query()函数引发异常,则调用close_db_connection()将永远不会执行,并且数据库连接将保持打开状态。该finally子句始终在执行 try all 异常处理程序后执行。以下是如何正确执行此操作:

def fetch_some_data():
    db = None
    try:
        db = open_db_connection()
        query(db)
    finally:
        if db is not None:
            close_db_connection(db)

调用open_db_connection()可能不会返回连接或本身引发异常。在这种情况下,无需关闭数据库连接。

使用时finally,您必须小心不要在那里引发任何异常,因为它们会掩盖原始异常。

上下文管理器

上下文管理器提供了另一种机制,可以将文件或数据库连接等资源包装在清理代码中,即使在引发异常时也会自动执行。with您使用语句而不是 try-finally 块。这是一个带有文件的示例:

def process_file(filename):
     with open(filename) as f:
        process(f.read())

现在,即使process()引发异常,当with退出块的范围时,文件将立即正确关闭,无论是否处理了异常。

日志记录

日志记录几乎是重要的、长时间运行的系统的要求。它在您可以以通用方式处理所有异常的 Web 应用程序中特别有用:只需记录异常并将错误消息返回给调用者。

记录时,记录异常类型、错误消息和堆栈跟踪很有用。所有这些信息都可以通过sys.exc_info对象获得,但是如果您logger.exception()在异常处理程序中使用该方法,Python 日志记录系统将为您提取所有相关信息。

这是我推荐的最佳做法:

import logging
logger = logging.getLogger()
 
def f():
    try:
        flaky_func()
    except Exception:
        logger.exception()
        raise

如果您遵循此模式,那么(假设您正确设置了日志记录)无论发生什么,您都会在日志中很好地记录问题所在,并且您将能够解决问题。

如果您再次加注,请确保您不会在不同级别一遍又一遍地记录相同的异常。这是一种浪费,它可能会让您感到困惑并让您认为同一问题的多个实例发生了,而实际上一个实例被多次记录。

最简单的方法是让所有异常传播(除非可以自信地处理并更早地吞下它们),然后在应用程序/系统的顶层附近进行日志记录。

哨兵

日志记录是一种能力。最常见的实现是使用日志文件。但是,对于拥有数百、数千或更多服务器的大型分布式系统,这并不总是最好的解决方案。

要跟踪整个基础架构中的异常情况,像sentry这样的服务非常有用。它集中了所有异常报告,除了堆栈跟踪之外,它还添加了每个堆栈帧的状态(引发异常时的变量值)。它还提供了一个非常好的界面,其中包含仪表板、报告和按多个项目分解消息的方法。它是开源的,因此您可以运行自己的服务器或订阅托管版本。

下面是一个屏幕截图,展示了 sentry 如何展示 Python 应用程序中的错误。

使用 Python 进行专业错误处理  第1张

这是导致错误的文件的详细堆栈跟踪。

使用 Python 进行专业错误处理  第2张

有些故障是暂时的,尤其是在处理分布式系统时。一个在出现问题的第一个迹象时就崩溃的系统并不是很有用。

如果您的代码正在访问某个没有响应的远程系统,传统的解决方案是超时,但有时并非每个系统都设计有超时。随着条件的变化,超时并不总是容易校准。

另一种方法是快速失败然后重试。好处是,如果目标快速响应,那么您不必在睡眠状态下花费大量时间,并且可以立即做出反应。但是如果它失败了,你可以重试多次,直到你确定它真的无法访问并引发异常。在下一节中,我将介绍一个可以为您完成的装饰器。

有用的装饰器

两个可以帮助处理错误的装饰器是@log_error,它记录一个异常然后重新引发它,以及@retry装饰器,它将重试调用一个函数几次。

错误记录器

这是一个简单的实现。装饰器除了记录器对象。当它装饰一个函数并调用该函数时,它将调用包装在一个 try-except 子句中,如果有异常,它将记录它并最终重新引发异常。

def log_error(logger)
    def decorated(f):
        @functools.wraps(f)
        def wrapped(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except Exception as e:
                if logger:
                    logger.exception(e)
                raise
        return wrapped
    return decorated

以下是如何使用它:

import logging
logger = logging.getLogger()
 
@log_error(logger)
def f():
    raise Exception('I am exceptional')

重试器

这是@retry 装饰器的一个非常好的实现。

import time
import math
 
# Retry decorator with exponential backoff
def retry(tries, delay=3, backoff=2):
  '''Retries a function or method until it returns True.
 
  delay sets the initial delay in seconds, and backoff sets the factor by which
  the delay should lengthen after each failure. backoff must be greater than 1,
  or else it isn't really a backoff. tries must be at least 0, and delay
  greater than 0.'''
 
  if backoff <= 1:
    raise ValueError("backoff must be greater than 1")
 
  tries = math.floor(tries)
  if tries < 0:
    raise ValueError("tries must be 0 or greater")
 
  if delay <= 0:
    raise ValueError("delay must be greater than 0")
 
  def deco_retry(f):
    def f_retry(*args, **kwargs):
      mtries, mdelay = tries, delay # make mutable
 
      rv = f(*args, **kwargs) # first attempt
      while mtries > 0:
        if rv is True: # Done on success
          return True
 
        mtries -= 1      # consume an attempt
        time.sleep(mdelay) # wait...
        mdelay *= backoff  # make future wait longer
 
        rv = f(*args, **kwargs) # Try again
 
      return False # Ran out of tries :-(
 
    return f_retry # true decorator -> decorated function
  return deco_retry  # @retry(arg[, ...]) -> true decorato

结论

错误处理对用户和开发人员都至关重要。Python 在语言和标准库中为基于异常的错误处理提供了强大的支持。通过努力遵循最佳实践,您可以克服这个经常被忽视的方面。


文章目录
  • 状态码与异常
  • 快速示例
  • Python 异常
  • 引发异常
  • 捕捉异常
    • 吞下例外
    • 重新引发相同的异常
    • 引发不同的异常
  • 最后条款
  • 上下文管理器
  • 日志记录
  • 哨兵
  • 有用的装饰器
    • 错误记录器
    • 重试器
  • 结论
  • 发表评论