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

Python用异常防止程序崩溃

2023-09-097.4k 阅读

Python异常处理基础

异常是什么

在Python编程中,异常是在程序执行期间发生的错误事件。当Python解释器遇到一个无法正常处理的情况时,就会引发异常。例如,当你尝试打开一个不存在的文件、进行除零运算或者访问列表中不存在的索引位置时,都会触发异常。如果不处理这些异常,程序就会崩溃并停止运行,同时会输出一个包含异常信息的追溯(traceback),显示错误发生的位置和原因。

异常的类型

Python中有许多内置的异常类型,每种类型代表特定类别的错误。以下是一些常见的异常类型:

  • SyntaxError:当Python解析器遇到语法错误时抛出。例如,代码中缺少冒号、括号不匹配等情况。
# 以下代码会引发SyntaxError
if 1 > 0
    print('True')

运行这段代码,Python解释器会提示 SyntaxError: invalid syntax,并指出错误发生的大概位置。

  • NameError:当尝试使用一个未定义的变量时抛出。
print(non_existent_variable)

这里由于 non_existent_variable 没有被定义,运行时会引发 NameError: name 'non_existent_variable' is not defined

  • TypeError:当操作或函数应用于不适当类型的对象时抛出。例如,将字符串和整数相加。
result = '5' + 5

这段代码会引发 TypeError: can only concatenate str (not "int") to str,因为Python不允许直接将字符串和整数进行连接操作。

  • ZeroDivisionError:当尝试除以零时抛出。
result = 10 / 0

这将导致 ZeroDivisionError: division by zero

  • FileNotFoundError:当尝试打开一个不存在的文件时抛出。
with open('non_existent_file.txt', 'r') as file:
    content = file.read()

运行这段代码,如果 non_existent_file.txt 文件不存在,就会引发 FileNotFoundError: [Errno 2] No such file or directory: 'non_existent_file.txt'

异常处理语句

try - except 语句

try - except 语句是Python中用于捕获和处理异常的基本结构。try 块中放置可能会引发异常的代码,except 块用于处理捕获到的异常。

try:
    num1 = 10
    num2 = 0
    result = num1 / num2
    print(result)
except ZeroDivisionError:
    print('不能除以零')

在上述代码中,try 块中的 num1 / num2 会引发 ZeroDivisionError 异常。由于存在 except ZeroDivisionError 块,程序不会崩溃,而是执行 except 块中的代码,输出 不能除以零

你也可以在一个 try - except 结构中处理多种类型的异常。可以使用多个 except 子句,每个子句处理一种特定类型的异常。

try:
    file = open('non_existent_file.txt', 'r')
    content = file.read()
    num = int('abc')
except FileNotFoundError:
    print('文件未找到')
except ValueError:
    print('无法将字符串转换为整数')

在这个例子中,try 块中的 open('non_existent_file.txt', 'r') 可能引发 FileNotFoundError,而 int('abc') 可能引发 ValueError。根据实际发生的异常,程序会执行相应的 except 块。

except 通用捕获

在某些情况下,你可能希望捕获所有类型的异常,而不区分具体的异常类型。可以使用不带异常类型的 except 子句,但这种方式应谨慎使用,因为它可能会掩盖代码中真正的问题。

try:
    result = 10 / 0
    file = open('non_existent_file.txt', 'r')
except:
    print('发生了一个异常')

这段代码会捕获 try 块中发生的任何异常,但无法得知具体是哪种异常,不利于调试和错误定位。通常,应该尽量明确捕获特定类型的异常,只有在确实无法预知会发生哪些异常的情况下,才使用通用的 except

except 捕获异常对象

except 子句可以捕获异常对象,通过在 except 关键字后指定一个变量来接收异常对象。这样可以获取关于异常的更多信息。

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

这里,e 就是捕获到的 ZeroDivisionError 异常对象,通过 {e} 可以将异常的具体信息打印出来,在这个例子中,会输出 发生了除零错误: division by zero

try - except - else 语句

try - except - else 结构在 try 块没有引发异常时执行 else 块中的代码。

try:
    num1 = 10
    num2 = 2
    result = num1 / num2
except ZeroDivisionError:
    print('不能除以零')
else:
    print(f'结果是: {result}')

在这个例子中,由于 num2 不为零,try 块中的除法运算不会引发异常,因此会执行 else 块,输出 结果是: 5.0

try - except - finally 语句

try - except - finally 结构中的 finally 块无论 try 块中是否引发异常,都会被执行。

try:
    file = open('example.txt', 'r')
    content = file.read()
except FileNotFoundError:
    print('文件未找到')
finally:
    if 'file' in locals():
        file.close()

在上述代码中,无论 open('example.txt', 'r') 是否成功,finally 块中的代码都会执行,关闭文件(如果文件成功打开)。这在处理文件、数据库连接等资源时非常有用,可以确保资源无论如何都会被正确释放。

自定义异常

为什么需要自定义异常

虽然Python内置了丰富的异常类型,但在实际开发中,有时内置异常不能准确描述特定业务场景下的错误。例如,在一个用户登录系统中,用户名不存在或者密码错误可能需要一种自定义的异常类型来表示,以便更好地进行错误处理和代码逻辑控制。

如何定义自定义异常

在Python中,自定义异常需要继承自内置的 Exception 类或其子类。

class UserNotFoundError(Exception):
    pass

class PasswordError(Exception):
    pass

def login(username, password):
    if username not in ['admin', 'user1']:
        raise UserNotFoundError('用户名不存在')
    elif password != 'correct_password':
        raise PasswordError('密码错误')
    else:
        print('登录成功')

try:
    login('non_existent_user', 'password')
except UserNotFoundError as e:
    print(e)
except PasswordError as e:
    print(e)

在这个例子中,定义了 UserNotFoundErrorPasswordError 两个自定义异常类,分别继承自 Exceptionlogin 函数根据不同的条件抛出相应的自定义异常,在 try - except 块中捕获并处理这些异常。

自定义异常的属性和方法

自定义异常类可以像普通类一样拥有属性和方法,以便在异常处理时提供更多信息。

class DatabaseError(Exception):
    def __init__(self, error_code, error_message):
        self.error_code = error_code
        self.error_message = error_message
    def __str__(self):
        return f'错误代码: {self.error_code}, 错误信息: {self.error_message}'

try:
    raise DatabaseError(1001, '数据库连接失败')
except DatabaseError as e:
    print(e)

这里的 DatabaseError 自定义异常类有 error_codeerror_message 两个属性,并通过 __str__ 方法自定义了异常的字符串表示。当捕获到该异常时,可以方便地获取详细的错误信息。

异常处理的最佳实践

避免过度捕获异常

虽然异常处理可以防止程序崩溃,但过度捕获异常可能会隐藏真正的问题。例如,使用通用的 except 而不进行特定异常类型的捕获,可能会捕获到一些原本应该暴露出来以便修复的严重错误。

# 不推荐的做法
try:
    # 一些复杂的数据库操作代码
    pass
except:
    # 简单处理,无法得知具体异常原因
    print('数据库操作出现问题')

更好的做法是明确捕获可能发生的异常类型,例如 DatabaseErrorConnectionError 等,这样可以根据不同的异常类型进行针对性的处理。

异常处理的层次结构

在大型项目中,异常处理应该有合理的层次结构。底层的函数或模块应该抛出具体的异常,而高层的调用者根据业务需求决定如何处理这些异常。

def divide_numbers(a, b):
    if b == 0:
        raise ZeroDivisionError('除数不能为零')
    return a / b

def calculate_result():
    try:
        result = divide_numbers(10, 0)
        print(result)
    except ZeroDivisionError:
        print('计算时发生除零错误,需要重新输入参数')

calculate_result()

在这个例子中,divide_numbers 函数抛出具体的 ZeroDivisionError 异常,calculate_result 函数捕获并处理这个异常。这种层次结构使得代码的错误处理逻辑更加清晰,易于维护。

记录异常信息

在处理异常时,记录异常信息对于调试和问题排查非常重要。Python的 logging 模块可以方便地记录异常信息。

import logging

try:
    result = 10 / 0
except ZeroDivisionError as e:
    logging.exception('发生除零错误')

运行这段代码,logging.exception 会记录异常的追溯信息,包括异常类型、错误信息以及错误发生的位置,方便开发者定位问题。

异常与性能

虽然异常处理是必要的,但频繁地引发和处理异常会影响程序的性能。因此,应该尽量在代码逻辑中避免可能引发异常的情况,只有在真正出现错误时才使用异常。

# 不推荐的做法,频繁引发异常
for i in range(10):
    try:
        result = 10 / i
    except ZeroDivisionError:
        result = None
    print(result)

# 推荐的做法,提前检查避免异常
for i in range(10):
    if i == 0:
        result = None
    else:
        result = 10 / i
    print(result)

在第一个例子中,每次循环都可能引发 ZeroDivisionError,而在第二个例子中,通过提前检查避免了异常的发生,提高了程序的性能。

异常处理在不同应用场景中的应用

文件操作中的异常处理

在进行文件操作时,如读取、写入、删除文件等,可能会遇到各种异常,如文件不存在、权限不足等。

try:
    with open('example.txt', 'r') as file:
        content = file.read()
        print(content)
except FileNotFoundError:
    print('文件未找到')
except PermissionError:
    print('没有权限访问该文件')

这里通过 try - except 结构处理了文件操作中可能出现的 FileNotFoundErrorPermissionError 异常,确保程序不会因为文件相关的错误而崩溃。

网络编程中的异常处理

在网络编程中,如使用 socket 模块进行网络连接时,可能会遇到连接超时、主机不可达等异常。

import socket

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(5)
    sock.connect(('non_existent_host', 80))
    sock.close()
except socket.gaierror:
    print('无法解析主机名')
except socket.timeout:
    print('连接超时')

在这个例子中,使用 try - except 捕获了 socket.gaierror(主机名解析错误)和 socket.timeout(连接超时)异常,使得程序在网络操作出现问题时能够优雅地处理,而不是崩溃。

数据库操作中的异常处理

在进行数据库操作,如连接数据库、执行SQL语句时,可能会遇到各种异常,如数据库连接失败、SQL语法错误等。

import sqlite3

try:
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM non_existent_table')
    rows = cursor.fetchall()
    for row in rows:
        print(row)
    conn.close()
except sqlite3.OperationalError as e:
    print(f'数据库操作错误: {e}')

这里使用 try - except 捕获了 sqlite3.OperationalError 异常,处理了数据库操作中可能出现的错误,如表不存在等情况,保证程序的稳定性。

总结异常处理的要点

在Python编程中,异常处理是确保程序健壮性和稳定性的重要手段。通过合理地使用 try - except 语句、自定义异常以及遵循异常处理的最佳实践,可以有效地防止程序因各种错误而崩溃。在不同的应用场景中,如文件操作、网络编程和数据库操作等,要根据具体可能出现的异常类型进行针对性的处理。同时,要注意避免过度捕获异常,合理记录异常信息,以及在代码逻辑中尽量减少不必要的异常引发,从而提高程序的性能和可维护性。掌握好异常处理技术,能够让你编写的Python程序更加可靠和强大。