MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python使用异常处理提高程序健壮性

2022-11-037.3k 阅读

Python异常处理基础概念

在Python编程中,异常是指程序在执行过程中出现的错误情况。当Python解释器遇到一个无法处理的错误时,它会引发一个异常。如果这个异常没有被适当处理,程序将会终止并显示错误信息。例如,当我们尝试访问一个不存在的文件时,Python会引发 FileNotFoundError 异常。

异常的类型

Python中有多种类型的异常,常见的包括:

  1. 语法错误(SyntaxError):这不是真正意义上运行时的异常,而是代码在编写过程中不符合Python语法规则导致的错误。例如,缺少冒号、括号不匹配等。如下代码:
if 1 > 0
    print('True')

这段代码中 if 语句后缺少冒号,就会报 SyntaxError

  1. 运行时错误(RuntimeError):这类异常在程序运行时发生。例如 ZeroDivisionError,当我们尝试除以零时会引发该异常:
result = 1 / 0

这里就会引发 ZeroDivisionError: division by zero 异常。

  1. 类型错误(TypeError):当操作或函数应用于不适当类型的对象时会引发此异常。比如,我们尝试将字符串和整数相加:
num = 1
text = 'abc'
sum_result = num + text

这会引发 TypeError: unsupported operand type(s) for +: 'int' and'str' 异常。

  1. 名称错误(NameError):当使用一个未定义的变量时会引发该异常。例如:
print(undefined_variable)

这会引发 NameError: name 'undefined_variable' is not defined 异常。

  1. 索引错误(IndexError):当尝试访问列表、元组或字符串中不存在的索引位置时会引发此异常。比如:
my_list = [1, 2, 3]
print(my_list[3])

这里会引发 IndexError: list index out of range 异常,因为列表索引从0开始,最大索引为2。

异常处理的基本语法

Python使用 try - except 语句来处理异常。基本语法如下:

try:
    # 可能会引发异常的代码块
    pass
except ExceptionType as e:
    # 处理异常的代码块
    pass

try 块中放置可能引发异常的代码。如果 try 块中的代码引发了异常,Python会立即跳转到相应的 except 块中执行异常处理代码。ExceptionType 是具体的异常类型,as e 是将异常对象赋值给变量 e,这样我们可以在异常处理代码中获取异常的详细信息。例如:

try:
    num1 = 10
    num2 = 0
    result = num1 / num2
except ZeroDivisionError as e:
    print(f"发生除零错误: {e}")

在上述代码中,try 块中的 num1 / num2 会引发 ZeroDivisionError 异常,程序会跳转到 except ZeroDivisionError 块中,打印出错误信息。

异常处理的进阶用法

多个except块

一个 try 语句可以跟随多个 except 块,用于处理不同类型的异常。例如,我们编写一个函数,该函数接收一个字符串并尝试将其转换为整数,如果字符串不是有效的数字格式,可能会引发不同类型的异常:

def convert_to_int(s):
    try:
        return int(s)
    except ValueError as e:
        print(f"值错误: {e},无法将 {s} 转换为整数")
    except TypeError as e:
        print(f"类型错误: {e},传入的不是字符串类型")


print(convert_to_int('10'))
print(convert_to_int('abc'))
print(convert_to_int(123))

在这个例子中,当传入有效的数字字符串时,函数正常返回整数。当传入无效的字符串(如 'abc')时,会引发 ValueError,由第一个 except 块处理。当传入非字符串类型(如 123)时,会引发 TypeError,由第二个 except 块处理。

通用except块

我们可以使用一个不带具体异常类型的 except 块来捕获所有类型的异常。例如:

try:
    num = 1 / 0
    result = num + 'abc'
except:
    print("发生了异常")

这种方式虽然能捕获所有异常,但不推荐在实际生产代码中大量使用,因为它无法区分不同类型的异常,不利于调试和错误处理的针对性。在大多数情况下,最好是捕获具体的异常类型。

finally块

finally 块无论 try 块中是否发生异常,都会被执行。例如:

try:
    file = open('test.txt', 'r')
    content = file.read()
except FileNotFoundError as e:
    print(f"文件未找到: {e}")
finally:
    if 'file' in locals():
        file.close()

在这个例子中,无论 open 函数是否成功打开文件(如果文件不存在会引发 FileNotFoundError),finally 块中的代码都会执行,确保文件被关闭。

使用异常处理提高程序健壮性的实际场景

文件操作

在进行文件读取和写入操作时,经常会遇到各种异常情况,如文件不存在、权限不足等。通过异常处理可以使程序更加健壮。

读取文件

def read_file_content(file_path):
    try:
        with open(file_path, 'r', encoding='utf - 8') as file:
            return file.read()
    except FileNotFoundError as e:
        print(f"文件 {file_path} 未找到: {e}")
        return ""
    except PermissionError as e:
        print(f"权限不足,无法读取文件 {file_path}: {e}")
        return ""


content = read_file_content('nonexistent_file.txt')
print(content)

在这个函数中,try 块使用 with 语句打开文件并读取内容。如果文件不存在,FileNotFoundError 异常会被捕获并处理,返回空字符串并打印错误信息。如果权限不足,PermissionError 异常会被捕获并处理,同样返回空字符串并打印错误信息。

写入文件

def write_to_file(file_path, content):
    try:
        with open(file_path, 'w', encoding='utf - 8') as file:
            file.write(content)
        print(f"成功写入文件 {file_path}")
    except PermissionError as e:
        print(f"权限不足,无法写入文件 {file_path}: {e}")
    except IsADirectoryError as e:
        print(f"{file_path} 是一个目录,无法写入: {e}")


write_to_file('test.txt', '这是写入的内容')
write_to_file('/root/test.txt', '尝试写入root目录')
write_to_file('/tmp', '尝试写入目录')

这里,try 块尝试将内容写入文件。如果权限不足,捕获 PermissionError 异常并打印错误信息。如果目标路径是一个目录,捕获 IsADirectoryError 异常并打印错误信息。

网络请求

在使用Python进行网络编程,比如发送HTTP请求时,也会遇到各种异常情况,如网络连接失败、超时等。以 requests 库为例:

import requests


def get_webpage_content(url):
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()
        return response.text
    except requests.exceptions.ConnectionError as e:
        print(f"连接错误: {e},无法连接到 {url}")
        return ""
    except requests.exceptions.Timeout as e:
        print(f"请求超时: {e},请检查网络或尝试增加超时时间")
        return ""
    except requests.exceptions.HTTPError as e:
        print(f"HTTP错误: {e},状态码: {response.status_code}")
        return ""


content = get_webpage_content('https://www.example.com')
content = get_webpage_content('https://nonexistentwebsite.com')
content = get_webpage_content('https://www.example.com/404')

在这个函数中,try 块发送HTTP GET请求,并使用 raise_for_status() 方法检查响应状态码。如果状态码不是200,会引发 HTTPError。如果连接失败,捕获 ConnectionError 异常。如果请求超时,捕获 Timeout 异常。每种异常都有相应的处理,返回空字符串并打印错误信息。

数据库操作

在进行数据库操作时,也会遇到各种异常,如连接失败、SQL语句错误等。以 sqlite3 库为例:

import sqlite3


def create_table():
    try:
        conn = sqlite3.connect('test.db')
        cursor = conn.cursor()
        cursor.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
        conn.commit()
        print("表创建成功")
    except sqlite3.OperationalError as e:
        print(f"操作错误: {e},可能表已存在")
    finally:
        if 'conn' in locals():
            conn.close()


create_table()
create_table()

在这个函数中,try 块尝试连接到SQLite数据库并创建一个表。如果表已经存在,会引发 OperationalError 异常,由 except 块处理并打印错误信息。无论是否发生异常,finally 块都会关闭数据库连接。

自定义异常

在某些情况下,Python内置的异常类型可能无法满足我们的需求,这时我们可以自定义异常。自定义异常需要继承自 Exception 类或它的子类。

定义自定义异常

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


def process_number(num):
    if num < 0:
        raise MyCustomError("数字不能为负数")
    return num * 2


try:
    result = process_number(-1)
except MyCustomError as e:
    print(f"捕获到自定义异常: {e}")

在这个例子中,我们定义了 MyCustomError 类,继承自 Exception。在 process_number 函数中,如果传入的数字为负数,就引发这个自定义异常。try - except 块捕获并处理这个自定义异常。

在复杂业务逻辑中使用自定义异常

假设我们正在开发一个电商系统,处理订单时可能会遇到各种特定业务异常,如库存不足、订单金额不符合要求等。

class InsufficientStockError(Exception):
    def __init__(self, product_name, available_stock, required_stock):
        self.product_name = product_name
        self.available_stock = available_stock
        self.required_stock = required_stock
        message = f"{product_name} 库存不足,现有库存: {available_stock},需要: {required_stock}"
        super().__init__(message)


class OrderAmountError(Exception):
    def __init__(self, order_amount, minimum_amount):
        self.order_amount = order_amount
        self.minimum_amount = minimum_amount
        message = f"订单金额 {order_amount} 小于最低金额 {minimum_amount}"
        super().__init__(message)


def process_order(product_name, quantity, order_amount):
    available_stock = 10
    minimum_amount = 100
    if quantity > available_stock:
        raise InsufficientStockError(product_name, available_stock, quantity)
    if order_amount < minimum_amount:
        raise OrderAmountError(order_amount, minimum_amount)
    print(f"订单处理成功,购买 {product_name} {quantity} 件,金额: {order_amount}")


try:
    process_order('商品A', 15, 80)
except InsufficientStockError as e:
    print(f"库存异常: {e}")
except OrderAmountError as e:
    print(f"金额异常: {e}")

在这个电商系统的示例中,我们定义了 InsufficientStockErrorOrderAmountError 两个自定义异常。process_order 函数根据库存和订单金额的条件引发相应的异常,try - except 块捕获并处理这些异常,使程序在面对特定业务错误时更加健壮。

异常处理与程序调试

利用异常信息进行调试

当程序引发异常时,异常信息包含了很多有用的调试线索。例如,异常类型、异常发生的位置等。假设我们有如下代码:

def divide_numbers(a, b):
    return a / b


try:
    result = divide_numbers(10, 0)
except ZeroDivisionError as e:
    import traceback
    print("捕获到除零错误,开始调试:")
    traceback.print_exc()

在这个例子中,traceback.print_exc() 函数会打印出异常的详细信息,包括异常类型、异常发生的文件名、函数名以及具体的代码行号。这对于定位和解决问题非常有帮助。

异常处理与日志记录

在实际项目中,结合日志记录可以更好地管理异常。Python的 logging 模块提供了强大的日志记录功能。例如:

import logging


def read_file(file_path):
    try:
        with open(file_path, 'r', encoding='utf - 8') as file:
            return file.read()
    except FileNotFoundError as e:
        logging.error(f"文件 {file_path} 未找到: {e}")
        return ""
    except PermissionError as e:
        logging.error(f"权限不足,无法读取文件 {file_path}: {e}")
        return ""


content = read_file('nonexistent_file.txt')

在这个代码中,当发生异常时,logging.error() 函数将错误信息记录到日志中。这样在程序运行过程中,我们可以通过查看日志文件来了解异常发生的情况,便于后续的调试和维护。

异常处理的性能考量

虽然异常处理能提高程序的健壮性,但在性能敏感的代码段中,需要注意异常处理可能带来的性能开销。例如,频繁地引发和捕获异常会降低程序的执行效率。因此,在编写代码时,应该尽量避免在循环内部等性能关键区域使用异常处理来处理正常的业务逻辑。比如,我们有如下代码:

import time


start_time = time.time()
for i in range(1000000):
    try:
        result = 1 / i
    except ZeroDivisionError:
        result = 0
print(f"使用异常处理耗时: {time.time() - start_time} 秒")


start_time = time.time()
for i in range(1000000):
    if i == 0:
        result = 0
    else:
        result = 1 / i
print(f"使用条件判断耗时: {time.time() - start_time} 秒")

在这个例子中,使用条件判断来避免除零错误的方式比使用异常处理在性能上更优。因此,在编写代码时,需要根据具体的业务场景和性能需求,合理选择使用异常处理还是条件判断来处理可能出现的错误情况。

通过合理运用Python的异常处理机制,包括基本的 try - except 语句、进阶的多个 except 块、finally 块,以及自定义异常等,结合异常处理在文件操作、网络请求、数据库操作等实际场景中的应用,再加上与程序调试和性能考量的结合,我们可以显著提高程序的健壮性,使程序在面对各种错误情况时更加稳定和可靠。同时,注意避免在性能敏感区域过度使用异常处理,确保程序在保持健壮性的同时,也能满足性能需求。