Python异常处理:基础到进阶的实用指南

news2024/11/26 21:30:41

前言

大家好,我是海鸽。异常处理在工程文件中必不可少,今天就带大家彻底搞定python的异常处理。

什么是异常

Python中,异常是指在程序执行过程中出现的错误或异常情况。

Python解释器无法执行代码时,它会引发异常,这可能是由于语法错误、运行时错误或逻辑错误等原因引起的。

我们经常会碰到诸如以下的异常:

SyntaxError异常

Python语言拥有自己的语法格式和规则。如果我们未能遵守这些规则,就会导致异常的出现。

In [52]: pint("Hello, World!")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [52], in <cell line: 1>()
----> 1 pint("Hello, World!")

NameError: name 'pint' is not defined

正确语法为print("Hello, World!")

NameError异常

NameError异常通常在代码中引用了未声明的变量或者不存在的函数或方法时触发。

In [53]: print(my_variable)  # 使用了未定义的变量
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [53], in <cell line: 1>()
----> 1 print(my_variable)

NameError: name 'my_variable' is not defined

KeyError异常

KeyError异常通常在尝试访问字典中不存在的键时触发。

In [54]: my_dict = {'a': 1, 'b': 2}
    ...: print(my_dict['c'])  # 尝试访问字典中不存在的键
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [54], in <cell line: 2>()
      1 my_dict = {'a': 1, 'b': 2}
----> 2 print(my_dict['c'])

KeyError: 'c'

FileNotFoundError(文件未找到错误)

FileNotFoundError异常通常在尝试打开或操作一个并不存在的文件时触发。

In [55]: file = open('non_existent_file.txt', 'r')
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Input In [55], in <cell line: 1>()
----> 1 file = open('non_existent_file.txt', 'r')

FileNotFoundError: [Errno 2] No such file or directory: 'non_existent_file.txt'

异常可以分为两种类型:

  1. 语法错误(Syntax Errors):这类错误也称为解析错误,是由于代码不符合Python语法规则而引起的。比如,拼写错误、缺少括号、不正确的缩进等。

  2. 运行时错误(Runtime Errors):这类错误在程序运行时发生,通常是由于程序的逻辑错误或外部环境引起的。常见的运行时错误包括除以零(ZeroDivisionError)访问不存在的索引(IndexError)使用未定义变量(NameError)等。

异常处理机制允许我们捕获并处理这些异常,以避免程序崩溃并提供错误处理和恢复的机会。

Python中,我们可以使用try-except语句来捕获并处理异常。try语句块用于执行可能会引发异常的代码,而except语句块用于处理异常情况。例如:

try:
    # 可能会引发异常的代码
    result = 10 / 0
except ZeroDivisionError:
    # 处理除以零的异常
    print("除数不能为零!")

除了使用try-except语句来处理异常,还可以使用其他相关的结构和关键字,如try-except-elsetry-except-finally等,来更灵活地处理异常情况。

异常的分类2

Python中,异常可以进一步分为内置异常(Built-in Exceptions)自定义异常(Custom Exceptions)

  1. 内置异常(Built-in Exceptions)Python提供了一系列内置的异常类来表示不同类型的错误或异常情况。这些异常类都是内置在Python解释器中的,开发者可以直接使用它们来处理程序执行过程中可能出现的各种错误。常见的内置异常包括:
-- SystemExit  # 解释器请求退出
-- KeyboardInterrupt  # 用户中断执行(通常是输入^C)
-- GeneratorExit  # 生成器(generator)发生异常来通知退出
     -- StopIteration  # 迭代器没有更多的值
     -- StopAsyncIteration  # 必须通过异步迭代器对象的__anext__()方法引发以停止迭代
     -- ArithmeticError  # 各种算术错误引发的内置异常的基类
     |    -- FloatingPointError  # 浮点计算错误
     |    -- OverflowError  # 数值运算结果太大无法表示
     |    -- ZeroDivisionError  # 除(或取模)零 (所有数据类型)
     -- AssertionError  # 当assert语句失败时引发
     -- AttributeError  # 属性引用或赋值失败
     -- BufferError  # 无法执行与缓冲区相关的操作时引发
     -- EOFError  # 当input()函数在没有读取任何数据的情况下达到文件结束条件(EOF)时引发
     -- ImportError  # 导入模块/对象失败
     |    -- ModuleNotFoundError  # 无法找到模块或在在sys.modules中找到None
     -- LookupError  # 映射或序列上使用的键或索引无效时引发的异常的基类
     |    -- IndexError  # 序列中没有此索引(index)
     |    -- KeyError  # 映射中没有这个键
     -- MemoryError  # 内存溢出错误(对于Python 解释器不是致命的)
     -- NameError  # 未声明/初始化对象 (没有属性)
     |    -- UnboundLocalError  # 访问未初始化的本地变量
     -- OSError  # 操作系统错误,EnvironmentError,IOError,WindowsError,socket.error,select.error和mmap.error已合并到OSError中,构造函数可能返回子类
     |    -- BlockingIOError  # 操作将阻塞对象(e.g. socket)设置为非阻塞操作
     |    -- ChildProcessError  # 在子进程上的操作失败
     |    -- ConnectionError  # 与连接相关的异常的基类
     |    |    -- BrokenPipeError  # 另一端关闭时尝试写入管道或试图在已关闭写入的套接字上写入
     |    |    -- ConnectionAbortedError  # 连接尝试被对等方中止
     |    |    -- ConnectionRefusedError  # 连接尝试被对等方拒绝
     |    |    -- ConnectionResetError    # 连接由对等方重置
     |    -- FileExistsError  # 创建已存在的文件或目录
     |    -- FileNotFoundError  # 请求不存在的文件或目录
     |    -- InterruptedError  # 系统调用被输入信号中断
     |    -- IsADirectoryError  # 在目录上请求文件操作(例如 os.remove())
     |    -- NotADirectoryError  # 在不是目录的事物上请求目录操作(例如 os.listdir())
     |    -- PermissionError  # 尝试在没有足够访问权限的情况下运行操作
     |    -- ProcessLookupError  # 给定进程不存在
     |    -- TimeoutError  # 系统函数在系统级别超时
     -- ReferenceError  # weakref.proxy()函数创建的弱引用试图访问已经垃圾回收了的对象
     -- RuntimeError  # 在检测到不属于任何其他类别的错误时触发
     |    -- NotImplementedError  # 在用户定义的基类中,抽象方法要求派生类重写该方法或者正在开发的类指示仍然需要添加实际实现
     |    -- RecursionError  # 解释器检测到超出最大递归深度
     -- SyntaxError  # Python 语法错误
     |    -- IndentationError  # 缩进错误
     |         -- TabError  # Tab和空格混用
     -- SystemError  # 解释器发现内部错误
     -- TypeError  # 操作或函数应用于不适当类型的对象
     -- ValueError  # 操作或函数接收到具有正确类型但值不合适的参数
     |    -- UnicodeError  # 发生与Unicode相关的编码或解码错误
     |         -- UnicodeDecodeError  # Unicode解码错误
     |         -- UnicodeEncodeError  # Unicode编码错误
     |         -- UnicodeTranslateError  # Unicode转码错误
     -- Warning  # 警告的基类
          -- DeprecationWarning  # 有关已弃用功能的警告的基类
          -- PendingDeprecationWarning  # 有关不推荐使用功能的警告的基类
          -- RuntimeWarning  # 有关可疑的运行时行为的警告的基类
          -- SyntaxWarning  # 关于可疑语法警告的基类
          -- UserWarning  # 用户代码生成警告的基类
          -- FutureWarning  # 有关已弃用功能的警告的基类
          -- ImportWarning  # 关于模块导入时可能出错的警告的基类
          -- UnicodeWarning  # 与Unicode相关的警告的基类
          -- BytesWarning  # 与bytes和bytearray相关的警告的基类
          -- ResourceWarning  # 与资源使用相关的警告的基类。被默认警告过滤器忽略。
异常捕获与处理

完整的内置异常列表可以在Python官方文档中找到。
https://docs.python.org/3/library/exceptions.html#bltin-exceptions

  1. 自定义异常(Custom Exceptions):除了使用Python提供的内置异常类外,开发者还可以根据自己的需求定义自己的异常类。通过定义自定义异常类,开发者可以更好地组织和管理程序中可能出现的特定类型的错误。自定义异常类通常继承自Python内置的Exception类或其子类。例如:
class CustomException(Exception):
    def __init__(self, message):
        self.message = message

# 在某个函数中引发自定义异常
def example_function(x):
    if x < 0:
        raise CustomException("输入不能为负数!")

# 使用try-except语句捕获自定义异常
try:
    example_function(-5)
except CustomException as e:
    print("捕获到自定义异常:", e.message)

这样就定义了一个名为CustomException的自定义异常类。

我们可以在程序中使用这个自定义异常来表示特定类型的错误,并根据需要在try-except语句中捕获和处理这些异常。

自定义异常的主要优势在于它能够使程序结构更清晰,能够更准确地表示程序中可能发生的异常情况,并能够根据具体情况提供更详细的错误信息。

异常处理的层次结构

python异常相关的关键字主要有:

异常处理的层次结构如下:

try:
    # 可能引发异常的代码块
    result = 1 / 0
except ZeroDivisionError as e:
    # 处理特定异常
    print(f"Error: {e}")
except Exception as e:
    # 处理其他异常
    print(f"Unexpected error: {e}")
else:
    # 如果没有异常发生且try中没有return时执行的代码
    print("No exceptions occurred.")
finally:
    # 无论是否发生异常都会执行的代码
    print("Finally block.")
  • 一般情况下,如果没有异常抛出以及错误,代码会执行的顺序是:try中代码块 =》else中代码块 =》finally中的代码块
  • 如果try中抛出了异常或者错误,此时执行的顺序是:try中的代码块 =》except捕获的异常代码块 =》finally中的代码块

finally块中的代码无论是否引发异常都会执行通常用于确保资源的释放或清理操作。

如何捕获多个异常?

Python中,我们可以使用多个except分别捕获不同类型的异常。

try:
    # 一些可能会引发异常的操作
    file = open('filename.txt', 'r')
    result = 10 / 0  # 这会引发一个 ZeroDivisionError
except FileNotFoundError as e:
    print(f"文件未找到错误: {e}")
except ZeroDivisionError as e:
    print(f"除以零错误: {e}")

在这个例子中,我们使用了两个except块,分别来捕获FileNotFoundErrorZeroDivisionError这两种不同类型的异常。当try块中的代码抛出任一种异常时,对应类型的except块就会捕获并处理该异常。

另一种方法是通过元组的方式一次性捕获多个异常类型。

try:
    # 一些可能会引发异常的操作
    file = open('filename.txt', 'r')
    result = 10 / 0  # 这会引发一个 ZeroDivisionError
except (FileNotFoundError, ZeroDivisionError) as e:
    print(f"发生错误: {e}")

在这个例子中,我们将多个异常类型放在一个元组中作为一个捕获异常的参数,这样就可以在一个except块中捕获多种类型的异常。

无论是逐个指定except块,还是使用一个元组指定多个异常类型,都可以帮助我们清晰地捕获并处理不同类型的异常。

异常的抛出

有些时候,我们可能需要主动抛出异常,比如:

  • 参数不符合我们设定的校验规则。
  • 捕获了异常,但是需要wrap一层更详细的报错或提示信息后继续抛出以方便上层处理。

Python中,要手动抛出异常,可以使用raise语句

def divide(x, y):
    if y == 0:
        raise ZeroDivisionError("除数不能为 0")
    return x / y

try:
    result = divide(10, 0)
    print(result)
except ZeroDivisionError as e:
    print(f"发生除以零错误:{e}")

在这个例子中,divide函数用于计算两个数相除,如果除数为0,则抛出一个ZeroDivisionError异常。在try块中调用了divide函数,并在except块中捕获了ZeroDivisionError异常。当除数为0时,divide函数会抛出异常,并且在except块中的代码会被执行。

通过raise语句,我们可以手动引发特定类型的异常,将其传播到调用栈中,并在需要的地方进行捕获和处理。

自定义异常

自定义异常通常被用于特定的情况或者错误类型,以便能够更清晰地识别和处理特定类型的问题。

以下是一个简单的自定义异常的示例:

class CustomException(Exception):
    def __init__(self, message="这是一个自定义异常"):
        self.message = message
        super().__init__(self.message)

在这个例子中,我们创建了一个名为CustomException的自定义异常类,它继承自Python的内置异常类Exception。我们定义了它的__init__方法来初始化异常的消息,并调用了父类的__init__方法以设置异常消息。

接下来,我们可以使用这个自定义异常类来抛出异常,并在需要捕获它的地方进行处理:

def some_function(x):
    if x < 0:
        raise CustomException("输入不能为负数")

try:
    some_function(-1)
except CustomException as e:
    print(f"捕获到了自定义异常:{e}")

在这个示例中,some_function函数根据输入的值是否小于0来抛出我们定义的CustomException自定义异常。在try...except块中,我们捕获并处理了这个自定义异常。

通过创建自定义异常类,您可以更好地组织和标识程序中特定类型的错误,从而实现更清晰、更易于维护的异常处理机制。

异常处理(函数):碰到return我该如何

  • 如果在函数内部做异常处理,此时需要考虑return
  • 带有finally子句的try语句内使用return语句时,finally子句始终在return声明。这确保了finally子句中的代码始终运行。

a. 无return时

from loguru import logger


def example_function(raise_error=True):
    try:
        logger.debug("在try语句块中")
        if raise_error:
            raise ValueError("主动抛出一个异常...")
        # return 1
    except:
        logger.error("在except语句块中")
        # return 2
    else:
        logger.debug("在else语句块中")
    finally:
        logger.debug("在finally语句块中")
        # return 3


example_function()
example_function(raise_error=False)

这个比较清晰,也比较容易理解。

我们看到:

  • 无异常时,会执行try里面的逻辑,然后是else里面的逻辑,最后是finally中的逻辑。
  • 异常时,会执行try里面异常前的逻辑,然后是finally中的逻辑,不会执行else中的代码。

b. 仅finally里面无return,无异常或有异常时

from loguru import logger


def example_function(raise_error=True):
    try:
        logger.debug("在try语句块中")
        if raise_error:
            raise IndexError("索引错误")
        return 1
    except:
        logger.debug("在except语句块中")
        return 2
    else:
        logger.debug("在else语句块中")
        return 2.5

    finally:
        logger.debug("在finally语句块中")
        # return 3


result = example_function()
logger.debug(f"result: {result}")

result2 = example_function(raise_error=False)  # 不会执行except语句块
logger.debug(f"result2: {result2}")

这个我们可能第一感觉是会直接报错,其实不然。不过pycharm中确实飘红了~

然后我们执行,我们看到这种情况下else中的逻辑如意想的一样,并不会执行:

这个else真的好“鸡肋”啊。

c. 都有return,无异常或有异常时

from loguru import logger


def example_function(raise_error=True):
    try:
        logger.debug("在try语句块中")
        if raise_error:
            raise IndexError("索引错误")
        return 1
    except:
        logger.debug("在except语句块中")
        return 2
    else:
        logger.debug("在else语句块中")
        return 2.5

    finally:
        logger.debug("在finally语句块中")
        return 3


result = example_function()
logger.debug(f"result: {result}")

result2 = example_function(raise_error=False)  # 不会执行except语句块
logger.debug(f"result2: {result2}")

可以看到,无论是否异常,也都不会执行else中的逻辑~

  • 无异常时,因为tryreturn了,所以不会执行else子句;
  • 有异常时,因为tryraise了异常,所以不会执行else

有异常或无异常时,分别走完tryexcept中的逻辑,最终都会执行finally中的逻辑,并且因为finally中存在return,返回值为finallyreturn结果。

d. 只有else中有return时

from loguru import logger


def example_function(raise_error=True):
    try:
        logger.debug("在try语句块中")
        if raise_error:
            raise IndexError("索引错误")
        # return 1
    except:
        logger.debug("在except语句块中")
        # return 2
    else:
        logger.debug("在else语句块中")
        return 2.5

    finally:
        logger.debug("在finally语句块中")
        # return 3


result = example_function()
logger.debug(f"result: {result}")

result2 = example_function(raise_error=False) 
logger.debug(f"result2: {result2}")
  • 异常的情况下,执行了try未异常代码 => except => finally的逻辑。
  • 无异常的情况下,执行了try => else => finally的逻辑,并执行了else中的return

e. 只有else和finally中有return时

#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:lianhaifeng
# __time__:2024/3/28 23:29
from loguru import logger


def example_function(raise_error=True):
    try:
        logger.debug("在try语句块中")
        if raise_error:
            raise IndexError("索引错误")
        # return 1
    except:
        logger.debug("在except语句块中")
        # return 2
    else:
        logger.debug("在else语句块中")
        return 2.5

    finally:
        logger.debug("在finally语句块中")
        return 3


result = example_function()
logger.debug(f"result: {result}")

result2 = example_function(raise_error=False)  # 不会执行except语句块
logger.debug(f"result2: {result2}")

警告警告:请尽量避免在finallyreturn!

异常处理的最佳实践

只做精准捕获异常

  • 始终只捕获那些潜在抛出异常的特定代码块。
  • 在捕获异常时,力求精确匹配具体的异常类型,避免笼统地使用通用的Exception

这样可以防止隐藏其他问题,并使代码更具可读性。

file = None
try:
    file = open('hello.py', 'rb')
    content = file.read()
    # 这里可以有其他可能引发异常的操作
except FileNotFoundError:
    print("文件未找到异常!")
except PermissionError:
    print("权限异常!")
except Exception as e:
    print("发生了一个异常:", e)
finally:
    if file is not None:
        file.close()

异常信息的记录

在处理异常时,最好记录异常的信息以便后续排查问题。可以使用标准库中的loggingtraceback模块或第三方库如:loguru

import logging
import traceback
from loguru import logger

try:
    # 代码块
    1 / 0
except Exception as e:
    traceback.print_exc()
    logging.exception("发生了异常:%s", e)
    logger.error(f"发生了异常:{e}")
    logger.exception(f"发生了异常:{e}")

慎用或不用else,避免在finally里return

在一些情况下,else可以被完全避免,通过在try块中放置需要运行成功的代码,以避免引入不必要的逻辑复杂度。

finally中使用return可能使代码变得难以维护。由于finally中的代码总是会执行,因此使用return可能会导致不符合预期的行为,降低代码的可维护性。

异常链的传递(raise…from)

异常链传递指的是当一个异常捕获并处理后,可以选择重新引发(re-raise)同一个异常或者另一个异常,并且保留原始异常的信息。

这种做法可以保留异常的上下文信息,使得调试和排查问题更加方便。

Python中,可以通过在异常处理程序中使用raise ... from语句来实现异常链的传递。例如,可以在捕获到某个异常后,重新引发同一个异常或者包装成新的异常并引发。

以下是一个示例,演示了异常链的传递:

try:
    # 可能会引发异常的代码
    x = 10 / 0
except ZeroDivisionError as original_exception:
    # 捕获并处理异常
    # 引发新的异常,保留原始异常的信息
    raise ValueError("除数不能为零") from original_exception

在上面的代码中,原始的ZeroDivisionError异常被捕获,并且使用raise ... from语句重新引发成ValueError异常,同时保留了原始异常的信息。这样做的好处是,即使异常被重新引发了,我们仍然可以通过异常链追踪到原始异常的信息,从而更好地理解异常的发生原因。

自定义异常(主动抛出异常raise)

Python中,自定义异常类通常是通过继承内置的Exception类或其子类来创建的。

通过自定义异常类,我们可以为特定的错误情况提供更具有描述性和语义化的异常类型,从而更好地组织和处理异常情况。

class CustomException(Exception):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message

def divide(a, b):
    if b == 0:
        raise CustomException("除数不能为零")
    return a / b

# 使用自定义异常
try:
    result = divide(10, 0)
except CustomException as e:
    print("捕获到自定义异常:", e)

在上面的示例中,我们创建了一个名为CustomException的自定义异常类,它继承自内置的Exception类。在异常类中,我们定义了__init__方法来初始化异常对象,并传入一个错误信息。另外,我们还重写了__str__方法,以便在打印异常对象时输出错误信息。

divide函数中,我们检查除数是否为零,如果除数为零,则抛出自定义的CustomException异常,并传入相应的错误信息。在异常处理块中,我们捕获并处理自定义异常,并打印出异常信息。

通过自定义异常,我们可以更好地组织和处理程序中可能出现的特定错误情况,提高了代码的可读性和可维护性。同时,它也使得程序的错误处理更加灵活和精确。

借助上下文管理器处理异常

异常处理不应该喧宾夺主!

上下文管理器是一种与with语句协同工作的特殊Python对象,它能够极大地简化异常处理流程,提升代码的优雅性和可读性。

我们来看一下python工匠里的例子:

def upload_avatar(request):
    """用户上传新头像"""
    try:
        avatar_file = request.FILES['avatar']
    except KeyError:
        raise error_codes.AVATAR_FILE_NOT_PROVIDED

    try:
       resized_avatar_file = resize_avatar(avatar_file)
    except FileTooLargeError as e:
        raise error_codes.AVATAR_FILE_TOO_LARGE
    except ResizeAvatarError as e:
        raise error_codes.AVATAR_FILE_INVALID

    try:
        request.user.avatar = resized_avatar_file
        request.user.save()
    except Exception:
        raise error_codes.INTERNAL_SERVER_ERROR
    return HttpResponse({})

类似代码在座的各位可能没少写~

这是一个处理用户上传头像的视图函数。这个函数内做了三件事情,并且针对每件事都做了异常捕获。如果做某件事时发生了异常,就返回对用户友好的错误到前端。

这样处理无可厚非,只是大量的try-except在主处理流程里使得我们一时间很难提炼出代码的核心逻辑,也不够pythonic

利用上下文管理器就能够使得我们的代码更加清晰可读,具体操作是:

class raise_api_error:
    """captures specified exception and raise ApiErrorCode instead

    :raises: AttributeError if code_name is not valid
    """
    def __init__(self, captures, code_name):
        self.captures = captures
        self.code = getattr(error_codes, code_name)

    def __enter__(self):
        # 该方法将在进入上下文时调用
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 该方法将在退出上下文时调用
        # exc_type, exc_val, exc_tb 分别表示该上下文内抛出的
        # 异常类型、异常值、错误栈
        if exc_type is None:
            return False

        if exc_type == self.captures:
            raise self.code from exc_val
        return False

接口的主要逻辑就变成下面这样:

def upload_avatar(request):
    """用户上传新头像"""
    with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'):
        avatar_file = request.FILES['avatar']

    with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
            raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
        resized_avatar_file = resize_avatar(avatar_file)

    with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
        request.user.avatar = resized_avatar_file
        request.user.save()
    return HttpResponse({})

简单明了了不少。


contextlib模块也提供了一些工具,用于支持与上下文管理器相关的操作。虽然contextlib模块本身并不直接用于异常处理,但它提供了一些用于创建上下文管理器的实用程序,这些上下文管理器可以帮助简化异常处理。

我们使用contextlib.contextmanager装饰器来创建一个简单的上下文管理器,用于捕获特定类型的异常:

import contextlib
from loguru import logger


@contextlib.contextmanager
def handle_file_exceptions():
    try:
        yield
    except FileNotFoundError as e:
        logger.exception(f"File not found error: {e}")
    except PermissionError as e:
        logger.exception(f"Permission error: {e}")


# 使用上下文管理器
with handle_file_exceptions():
    with open('nonexistent_file.txt', 'r') as file:
        content = file.read()

在这个例子中,handle_file_exceptions函数使用contextlib.contextmanager装饰器转变为一个上下文管理器。在调用yield之前的代码段被看作是上下文管理器的__enter__方法,在yield之后的代码段被看作是上下文管理器的__exit__方法。在__exit__方法中,我们捕获了FileNotFoundErrorPermissionError,并打印了相应的错误信息。

虽然contextlib模块的主要目的是创建上下文管理器,但它也可以与异常处理一起使用,使得在特定上下文情境中进行异常处理变得更加方便和简洁。

使用装饰器处理异常

异常处理装饰器是另一种用于简化异常处理逻辑的技术,它可以在函数调用时自动捕获并处理异常,从而减少重复代码量,提高代码的可维护性和可读性。

from loguru import logger


def handle_exception(exception_handler):
    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                return exception_handler(e, *args, **kwargs)

        return wrapper

    return decorator


# 自定义异常处理函数
def custom_exception_handler(exception, *args, **kwargs):
    # 这里可以进行自定义的异常处理逻辑
    if isinstance(exception, ZeroDivisionError):
        logger.error(f"捕获到异常:{exception}")

    return None  # 返回处理结果或者 None


# 使用异常处理装饰器
@handle_exception(custom_exception_handler)
def divide(a, b):
    return a / b


result = divide(10, 1)
logger.info(f"result: {result}")
# 调用带有异常处理装饰器的函数
result2 = divide(10, 0)
logger.info(f"result2: {result2}")

使用异常处理装饰器可以使代码更加简洁、清晰,并且能够提高代码的复用性和可维护性。
注意: 异常处理装饰器的具体实现可以根据实际需求进行定制和扩展,以满足不同场景的需求。

from functools import wraps
from typing import Callable, Dict, Type
from loguru import logger


class TryDecorator:
    def __init__(self):
        self.exception_handlers: Dict[Type[Exception], Callable] = {}

    def try_(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                handler = self.get_exception_handler(type(e))
                if handler is None:
                    raise e
                return handler(func, e, *args, **kwargs)

        return wrapper

    def except_(self, *exceptions: Type[Exception]) -> Callable:
        def decorator(handler: Callable) -> Callable:
            for exception in exceptions:
                self.exception_handlers[exception] = handler
            return handler

        return decorator

    def get_exception_handler(self, exception_type: Type[Exception]) -> Callable:
        for exception, handler in self.exception_handlers.items():
            if issubclass(exception_type, exception):
                return handler
        return lambda *args, **kwargs: None  # 返回一个默认的异常处理函数,可以根据需要修改


try_decorator = TryDecorator()


@try_decorator.try_
def my_function():
    logger.info(1 / 0)
    logger.info('hello world')


@try_decorator.try_
def my_function2():
    raise IndexError("主动抛出了IndexError")


@try_decorator.except_(ZeroDivisionError)
def handle_zero_division_error(func: Callable, exception: Exception, *args, **kwargs):
    logger.error(f"ZeroDivisionError in function {func.__name__}: {exception}")


@try_decorator.except_(IndexError)
def handle_zero_division_error(func: Callable, exception: Exception, *args, **kwargs):
    logger.error(f"IndexError in function {func.__name__}: {exception}")


if __name__ == '__main__':
    my_function()
    my_function2()

让你的函数返回一些有意义的、有类型的、安全的东西!

对于python函数的异常处理往往令我们比较的头疼。

比如,可能你的函数的正常流程是返回一个字符串,异常的时候是直接raise抛出呢还是返回一个特定值?这是一个问题!

传统的异常处理机制依赖于在函数内部抛出异常,然后在调用函数的地方进行捕获和处理。
这种方式可能会导致代码分散、难以维护,同时将错误处理的责任分散到多个地方。异常可能会中断程序的正常流程,使得代码复杂和难以预测。

聪明如你,可能自己封装一个统一的结构化响应

统一响应:谁弄(异)哭(常)的,谁就要负责哄(包装)~

不过我要告诉你的是,使用returns模块可以将函数的结果值进行封装,这样就可以更灵活地处理异常情况。

returns模块提供了SuccessFailure两种类型,分别代表函数成功执行和出现错误。这就使得函数返回的结果更加明确,可以更容易地进行组合、转换和处理。

安装

pip install returns

我们改写下使用装饰器处理异常部分的示例代码:

from typing import Callable
from returns.result import Success, Failure, Result
from loguru import logger
from functools import wraps


class TryDecorator:
    def __init__(self):
        # 初始化异常处理程序字典
        self.exception_handlers = {}

    def try_(self, func: Callable) -> Callable[..., Result]:
        @wraps(func)
        def wrapper(*args, **kwargs) -> Result:
            try:
                result = func(*args, **kwargs)
                # 如果返回结果已经是 Result 类型,则直接返回
                if isinstance(result, Result):
                    return result
                # 否则将返回结果包装成 Success 类型
                return Success(result)
            except Exception as e:
                # 获取对应异常类型的处理函数
                handler = self.get_exception_handler(type(e))
                if handler is None:
                    # 如果没有找到对应的异常处理函数,则直接返回 Failure
                    return Failure(e)
                # 调用对应异常处理函数处理异常
                return handler(func, e, *args, **kwargs)

        return wrapper

    def except_(self, *exceptions: type) -> Callable:
        def decorator(handler: Callable) -> Callable:
            # 为每个异常类型关联对应的处理函数
            for exception in exceptions:
                self.exception_handlers[exception] = handler
            return handler

        return decorator

    def get_exception_handler(self, exception_type: type) -> Callable:
        # 获取对应异常类型的处理函数
        for exception, handler in self.exception_handlers.items():
            if issubclass(exception_type, exception):
                return handler
        # 如果没有找到对应异常的处理函数,则返回一个默认的异常处理函数,可以根据需要修改
        return lambda *args, **kwargs: None


try_decorator = TryDecorator()


@try_decorator.except_(ZeroDivisionError)
def handle_zero_division_error(func: Callable, exception: Exception, *args, **kwargs) -> Result:
    # 处理 ZeroDivisionError 异常,并返回 Failure
    logger.error(f"ZeroDivisionError in function {func.__name__}: {exception}")
    return Failure(exception)


@try_decorator.except_(IndexError)
def handle_index_error(func: Callable, exception: Exception, *args, **kwargs) -> Result:
    # 处理 IndexError 异常,并返回 Failure
    logger.error(f"IndexError in function {func.__name__}: {exception}")
    return Failure(exception)


@try_decorator.try_
def my_function(a, b):
    return Success(a / b)


@try_decorator.try_
def my_function2():
    raise IndexError("主动抛出了IndexError")


if __name__ == '__main__':
    # 测试 try_decorator 的 try_ 方法
    # result1 = try_decorator.try_(my_function)(10, 1)
    result1 = my_function(10, 1)
    if isinstance(result1, Success):
        logger.info(f"没有异常:{result1}, result1: {result1.unwrap()}")

    # 测试 ZeroDivisionError 异常处理
    result2 = my_function(10, 0)
    if isinstance(result2, Failure):
        logger.info(f"发生异常:{result2}, result2: {result2.failure()}")

    result3 = my_function2()
    if isinstance(result3, Failure):
        logger.error(f"Failed with: {result3.failure()}")

是不是清爽了许多,输出结果为:

  • returns模块促进了函数式编程风格的使用。
  • 使用returns模块可以更容易地编写测试,因为你可以明确定义函数的行为并检查所得的结果。
  • 使用返回值进行异常处理能够提供更多的灵活性和可预测性。通过返回值传递错误信息,调用者可以决定是否要处理错误,以及如何处理错误。

更多returns特性请阅读:
github: https://github.com/dry-python/returns

更多场景举例

只做示例介绍,不一定遵循最佳实践。

基本的异常处理重试装饰器

下面是一个基本的异常处理重试装饰器的示例:

import time
from functools import wraps
from typing import Callable


def retry_on_exception(max_retries: int, delay: float = 0.1):
    def decorator(func: Callable):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Exception caught: {e}. Retrying...")
                    time.sleep(delay)
            raise Exception(f"Failed after {max_retries} retries")

        return wrapper

    return decorator


# 使用示例
@retry_on_exception(max_retries=3)
def potentially_unstable_function():
    import random
    if random.randint(1, 10) < 8:
        raise ValueError("Random error")
    else:
        return "Success"


if __name__ == "__main__":
    result = potentially_unstable_function()
    print(result)

在这个示例中,retry_on_exception是一个装饰器工厂函数,它接受最大重试次数和延迟作为参数。它返回一个装饰器,该装饰器会捕获被装饰函数可能抛出的异常,并在遇到异常时进行重试。

被装饰的函数potentially_unstable_function可能会抛出随机异常。我们使用retry_on_exception装饰器装饰它,以便在遇到异常时进行重试,最多重试三次。

你可以根据需要调整max_retriesdelay参数来控制重试的次数和重试之间的延迟。

异常优雅重试

其实,很多轮子已经是卷中卷了,我们大可使用优雅的retrying模块。

pip install retrying

使用也很简单:

from retrying import retry
import random
from loguru import logger

attempt = 0


# 重试装饰器
@retry(stop_max_attempt_number=3, wait_fixed=100)  # 最多重试3次,每次间隔100ms
def potentially_unstable_function():
    global attempt
    attempt += 1
    if random.randint(1, 10) < 8:
        logger.error(attempt)
        raise ValueError("Random error")
    else:
        return "Success"


if __name__ == "__main__":
    result = potentially_unstable_function()
    logger.info(result)

这里为了演示确实最大重试了stop_max_attempt_number=3,加了attempt,不然代码只会更少!

超过3次时报错如下:

总结

恭喜毕业!异常处理是编写稳健、可维护Python代码的重要组成部分。以下是异常处理的关键点总结:

  1. 捕获最具体的异常:为了更好地理解和处理错误,请捕获最具体的异常类型。

  2. 不要使用单独的except处理所有异常:避免使用空的except块,因为它们会捕获所有异常,包括意外的情况,会增加调试的困难。

  3. 谨慎使用elsefinally块:else块用于在没有异常时执行特定代码,finally块用于确保资源释放或清理操作。

  4. 记录异常信息:捕获异常后,通常应记录异常信息,以便诊断和修复问题。

  5. 合理使用自定义异常: 当您的应用程序遇到特定异常条件时,建议创建一个自定义异常类,可以更清晰地表达和有效处理这些情况。

  6. 避免不必要的异常处理:不应将异常处理用于预期的控制流,而应只在真正可能发生异常的地方使用它。

  7. 使用装饰器,上下文管理器,returns库使你的异常处理更加的pythonic

最后

如果你觉得文章还不错,请大家点赞分享留言,因为这将是我持续输出更多优质文章的最强动力!

参考

https://returns.readthedocs.io/en/latest/pages/result.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1561058.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Excel 粘贴回筛选后的单元格不能完全粘老是少数据 ,有些单元格还是空的

环境&#xff1a; excel2021 Win10专业版 问题描述&#xff1a; excel 粘贴回筛选后的单元格不能完全粘老是少数据 有些单元格还是空的 复制选择筛选后A1-A10单元格 &#xff0c;定位条件&#xff09;&#xff08;仅可见单元格&#xff09;来访问&#xff0c;或者你可以使用…

算法沉淀 —— 深度搜索(dfs)

算法沉淀 —— 深度搜索&#xff08;dfs&#xff09; 一、计算布尔二叉树的值二、求根节点到叶节点数字之和三、二叉树剪枝四、验证二叉搜索树五、二叉搜索树中第K小的元素 一、计算布尔二叉树的值 【题目链接】&#xff1a;2331. 计算布尔二叉树的值 【题目】&#xff1a; …

MambaMixer、NeSLAM、Talk3D、BundledSLAM、ShapeFusion

本文首发于公众号&#xff1a;机器感知 MambaMixer、NeSLAM、Talk3D、BundledSLAM、ShapeFusion ShapeFusion: A 3D diffusion model for localized shape editing In the realm of 3D computer vision, parametric models have emerged as a ground-breaking methodology fo…

全套医院手术麻醉系统源码 人工智能麻醉系统源码 医疗管理系统源码

全套医院手术麻醉系统源码 人工智能麻醉系统源码 医疗管理系统源码 手术麻醉临床信息系统有着完善的临床业务功能&#xff0c;能够涵盖整个围术期的工作&#xff0c;能够采集、汇总、存储、处理、展现所有的临床诊疗资料。通过该系统的实施&#xff0c;能够规范麻醉科的工作流…

Qt实现通过.css样式文件实时加载QSS样式

初学Qt时要想通过QSS修改控件QWidget&#xff0c;QPushButton等原生基础控件的样式&#xff0c;一般都是直接在.ui文件中直接添加qss&#xff0c;或者在代码中通过setStyleSheet(QString qss)来设置。当程序很大时&#xff0c;很多地方需要复用样式时会非常麻烦&#xff0c;qss…

C++教学——从入门到精通 6.ASCII码与字符型

如何把小写字母转换成大写字母呢&#xff1f; 这个问题问的好&#xff0c;首先我们要新学一个类型——char 这个类型就是字符型 再来说说ASCII码 给大家举几个例子 空格————32 0————48 9————57 A————65 Z————90 a————97 z————122 我们…

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】 题目描述&#xff1a;解题思路一&#xff1a;一边算前缀和一边统计。这里用哈希表统计前缀和出现的次数&#xff0c;那么和为k的子数组的个数就是当前前缀和-k的个数&#xff0c;即preSums[presum - k]。画个图表述就是&a…

NGINX 反向代理 CORS

我遇到了一个问题就是 Nginx 是作为反向代理服务器部署的&#xff0c;但因为 Nginx 的配置导致 CORS 问题。 在这个时候我们可以对 Nginx 的配置文件进行修改&#xff1a; 在 location 后添加下面的内容&#xff1a; add_header Access-Control-Allow-Origin *; add_header A…

Kafka入门到实战-第二弹

Kafka入门到实战 Kafka快速开始官网地址Kafka概述Kafka术语Kafka初体验更新计划 Kafka快速开始 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流…

ES6学习之路:迭代器Iterator和生成器Generator

迭代器 一、知识背景 什么是迭代器 迭代器就是在一个数据集合中不断取出数据的过程迭代和遍历的区别 遍历是把所有数据都取出迭代器注重的是依次取出数据&#xff0c;它不会在意有多少数据&#xff0c;也不会保证能够取出多少或者能够把数据都取完。比如斐波那契额数列&#…

离散时间动态系统的集成自适应动态规划智能控制-北科大博毕

主要内容&#xff1a; 传统值迭代产生迭代控制策略&#xff0c;给出稳定性和吸引域判据&#xff1b;传统值迭代则迭代过程中得到可容许策略折扣因子对迭代控制策略可容许的影响&#xff0c;神经网络对未知系统建模&#xff0c;讨论模型网络权重更新情况下参数误差和系统状态估…

TikTok零播放?可能是海外代理IP的问题

在当今社交媒体的蓬勃发展中&#xff0c;TIKTOK作为一款备受欢迎的短视频平台&#xff0c;其直播功能也逐渐受到用户的青睐。然而&#xff0c;有时候跨境电商商家在进行直播时却面临着一个令人头疼的问题&#xff1a;没有观众。这时候&#xff0c;海外代理IP可能是一个潜在的原…

吴恩达:别光盯着GPT-5,用GPT-4做个智能体可能提前达到GPT-5的效果

最近&#xff0c;斯坦福大学教授吴恩达在演讲中提到&#xff0c;他们发现&#xff0c;基于 GPT-3.5 构建的智能体工作流在应用中表现比 GPT-4 要好。 AI 智能体是去年很火的一个话题&#xff0c;但是 AI 智能体到底有多大的潜力&#xff0c;很多人可能没有概念。 最近&#x…

【环境搭建】(四)ubuntu22.04系统安装Opencv4.8.0+Opencv-contrib4.8.0

一个愿意伫立在巨人肩膀上的农民...... 一、安装下载所需工具 1.打开终端&#xff0c;输入以下命令来更新软件源&#xff1a; sudo apt-get update 2.安装wget&#xff1a; sudo apt-get install wget 3.下载opencv和opencv-contrib包&#xff1a; wget -O opencv-4.8.0.…

ISP-VPN实验

文章目录 ISP-VPN实验一&#xff0c;实验拓扑二、实验要求三、IP规划四、实验配置1、IP配置R1的配置R2的配置R3的配置R4的配置R5的配置 2、配置缺省路由3、认证与被认证配置4、HDLC封装5、构建MGRE和GRE6、整个私有网络基于RIP全网可达7、查看路由配置和PC端配置8、PC端pingR5的…

Vue element-plus 导航栏 [el-menu]

导航栏 [el-menu] Menu 菜单 | Element Plus el-menu有很多属性和子标签&#xff0c;为网站提供导航功能的菜单。 常用标签&#xff1a; 它里面有两个子标签。el-menu-item&#xff0c;它其实就是el-menu每一个里面的item&#xff0c;item就是真实匹配到路由的每个栏目&#…

【Python使用】嘿马头条完整开发md笔记第3篇:数据库,1 新增【附代码文档】

嘿马头条项目从到完整开发笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;课程简介&#xff0c;ToutiaoWeb虚拟机使用说明1 产品介绍,2 原型图与UI图,3 技术架构,4 开发,1 需求,2 注意事项。数据库&#xff0c;理解ORM1 简介,2 安装,3 数据库连接…

使用CRXjs、Vite、Vue 开发 Chrome 多页面插件,手动配置 vite.config.ts 和 manifest.json 文件

一、使用CRXjs、Vite、Vue 开发 Chrome 多页面插件&#xff0c;手动配置 vite.config.ts 和 manifest.json 文件 一、创建 Vue 项目 1. 使用 Vite 创建 Vue 项目 npm create vitelatest # npm yarn create vite # yarn pnpm create vite # pnpm选择 Vue 和 TS 进入项目…

SpringBoot微服务实现深度学习:构建AGI道路的基石+实战案例演示

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

4月4日生效!管控升级!继续围堵 | 百能云芯

据路透社29日报道&#xff0c;美国拜登政府当天修改规则&#xff0c;升级针对中国人工智能&#xff08;AI&#xff09;芯片和相关工具出口管制的措施。报道声称&#xff0c;这是美国以国家安全为由阻碍中国芯片制造业发展的努力的一部分。 美国商务部下属的工业与安全局&#x…