Python教程26:异常处理

“预则立,不预则废。”

程序运行时难免会遇到错误:文件不存在、网络断开、用户输入非法数据。异常处理机制让程序能够优雅地应对这些错误,而不是直接崩溃。今天我们学习Python的异常处理。

1. 什么是异常

错误vs异常

语法错误(Syntax Error)

  • 代码写错了,Python无法解析
  • 程序无法运行
  • 例如:忘记冒号、括号不匹配
1# 语法错误示例
2if True  # SyntaxError: 缺少冒号
3    print("Hello")

异常(Exception)

  • 语法正确,但运行时出错
  • 可以被捕获和处理
  • 例如:除零、文件不存在、类型错误
1# 异常示例
2x = 1 / 0  # ZeroDivisionError: division by zero

为什么需要异常处理

没有异常处理

1def divide(a, b):
2    return a / b
3
4result = divide(10, 0)  # 程序崩溃!
5print("这行代码不会执行")

有异常处理

1def divide(a, b):
2    try:
3        return a / b
4    except ZeroDivisionError:
5        print("错误:除数不能为0")
6        return None
7
8result = divide(10, 0)
9print("程序继续运行")  # 这行会执行

异常处理的作用

  • 防止程序崩溃
  • 提供友好的错误信息
  • 执行清理操作(关闭文件、释放资源)
  • 记录错误日志
  • 优雅降级(功能不可用时提供备选方案)

2. 基本语法:try-except

1try:
2    # 可能出错的代码
3    risky_operation()
4except ExceptionType:
5    # 处理异常的代码
6    handle_error()

简单示例

1try:
2    num = int(input("请输入数字:"))
3    result = 10 / num
4    print(f"结果:{result}")
5except ValueError:
6    print("错误:请输入有效的数字")
7except ZeroDivisionError:
8    print("错误:除数不能为零")

执行流程

  1. 执行try块中的代码
  2. 如果没有异常:跳过except块
  3. 如果有异常:跳到对应的except块
  4. except块执行完后,程序继续

3. 常见异常类型

Python内置了很多异常类,都继承自BaseException

 1# ZeroDivisionError - 除零错误
 2# 当除数或模数为零时抛出
 3try:
 4    result = 10 / 0
 5except ZeroDivisionError as e:
 6    print(f"除零错误:{e}")
 7
 8# ValueError - 值错误
 9# 函数接收了正确类型但值不合适的参数
10try:
11    num = int("abc")
12except ValueError as e:
13    print(f"值错误:{e}")
14
15# TypeError - 类型错误
16# 操作应用于不适当类型的对象
17try:
18    result = "2" + 2
19except TypeError as e:
20    print(f"类型错误:{e}")
21
22# FileNotFoundError - 文件不存在  
23# 尝试打开不存在的文件时
24try:
25    with open("nonexistent.txt") as f:
26        content = f.read()
27except FileNotFoundError as e:
28    print(f"文件未找到:{e}")
29
30# KeyError - 键错误
31# 字典中不存在该键
32try:
33    d = {"name": "Alice"}
34    print(d["age"])
35except KeyError as e:
36    print(f"键不存在:{e}")
37
38# IndexError - 索引错误
39# 序列索引超出范围
40try:
41    numbers = [1, 2, 3]
42    print(numbers[10])
43except IndexError as e:
44    print(f"索引错误:{e}")
45
46# AttributeError - 属性错误
47# 对象没有该属性或方法
48try:
49    x = 5
50    x.append(1)
51except AttributeError as e:
52    print(f"属性错误:{e}")

异常层次结构

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── StopIteration
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── ValueError
    ├── TypeError
    └── ...

4. 捕获多个异常

方法1:多个except块

1try:
2    # 各种可能的操作
3    result = risky_operation()
4except ValueError:
5    print("值错误")
6except TypeError:
7    print("类型错误")
8except KeyError:
9    print("键错误")

方法2:一个except捕获多种异常

1try:
2    result = risky_operation()
3except (ValueError, TypeError, KeyError) as e:
4    print(f"发生错误:{e}")

方法3:捕获所有异常(不推荐)

1try:
2    result = risky_operation()
3except Exception as e:  # Exception是大多数异常的基类
4    print(f"发生未知错误:{e}")

注意

  • 不要用except:捕获所有异常(包括SystemExit、KeyboardInterrupt)
  • 尽量捕获具体的异常类型
  • Exception捕获大部分异常,但不包括系统异常

5. else子句

else块在try块没有异常时执行:

1try:
2    num = int(input("请输入数字:"))
3except ValueError:
4    print("输入无效")
5else:
6    # 只有输入有效时才执行
7    print(f"你输入的数字是:{num}")
8    result = num * 2
9    print(f"两倍是:{result}")

为什么用else

  • 分离正常代码和异常处理代码
  • 让异常范围更精确
  • 代码结构更清晰
 1# 不好的写法
 2try:
 3    file = open("data.txt")
 4    content = file.read()
 5    data = process(content)  # process也在try中
 6except FileNotFoundError:
 7    print("文件不存在")
 8
 9# 好的写法
10try:
11    file = open("data.txt")
12except FileNotFoundError:
13    print("文件不存在")
14else:
15    # 文件打开成功后的处理
16    content = file.read()
17    data = process(content)

6. finally子句

finally块无论是否发生异常都会执行:

 1try:
 2    file = open("data.txt")
 3    content = file.read()
 4except FileNotFoundError:
 5    print("文件不存在")
 6finally:
 7    # 无论如何都会执行
 8    print("清理操作")
 9    if 'file' in locals():
10        file.close()

finally的用途

  • 释放资源(关闭文件、网络连接)
  • 清理临时数据
  • 日志记录
  • 确保某些代码一定执行

执行顺序

 1try:
 2    print("1. try块")
 3    # raise Exception()  # 取消注释测试异常情况
 4    print("2. try块继续")
 5except Exception:
 6    print("3. except块")
 7else:
 8    print("4. else块")
 9finally:
10    print("5. finally块")
11print("6. 后续代码")
12
13# 无异常:1 -> 2 -> 4 -> 5 -> 6
14# 有异常:1 -> 3 -> 5 -> 6

7. 抛出异常:raise

主动抛出异常

 1def set_age(age):
 2    """设置年龄"""
 3    if age < 0:
 4        raise ValueError("年龄不能为负数")
 5    if age > 150:
 6        raise ValueError("年龄不合理")
 7    print(f"年龄设置为:{age}")
 8
 9try:
10    set_age(-5)
11except ValueError as e:
12    print(f"错误:{e}")

重新抛出异常

1try:
2    file = open("data.txt")
3except FileNotFoundError:
4    print("文件不存在,记录错误")
5    raise  # 重新抛出同一个异常

异常链

1try:
2    result = 10 / 0
3except ZeroDivisionError as e:
4    # from用于保留原始异常
5    raise ValueError("计算失败") from e

8. 自定义异常

创建自己的异常类:

 1class InvalidAgeError(Exception):
 2    """
 3    自定义异常:年龄无效
 4    - 继承Exception或其子类
 5    - 添加自定义属性和方法
 6    """
 7    def __init__(self, age, message="年龄无效"):
 8        self.age = age
 9        self.message = message
10        super().__init__(self.message)
11    
12    def __str__(self):
13        return f"{self.message}{self.age}"
14
15# 使用自定义异常
16def set_age(age):
17    if not isinstance(age, int):
18        raise TypeError("年龄必须是整数")
19    if age < 0 or age > 150:
20        raise InvalidAgeError(age, "年龄不在合理范围")
21    return age
22
23try:
24    set_age(-5)
25except InvalidAgeError as e:
26    print(f"自定义异常:{e}")

何时自定义异常

  • 业务逻辑需要特定的异常类型
  • 需要携带额外信息
  • 方便异常分类处理
  • 提高代码可读性

9. 异常处理最佳实践

1. 具体异常优先

 1# 好:具体异常
 2try:
 3    num = int(input())
 4except ValueError:
 5    print("请输入数字")
 6
 7# 不好:过于宽泛
 8try:
 9    num = int(input())
10except Exception:  # 捕获所有异常
11    print("出错了")

2. 不要忽略异常

 1# 不好:静默失败
 2try:
 3    risky_operation()
 4except Exception:
 5    pass  # 什么都不做,错误被隐藏
 6
 7# 好:至少记录日志
 8import logging
 9
10try:
11    risky_operation()
12except Exception as e:
13    logging.error(f"操作失败:{e}")

3. 缩小try范围

 1# 不好:try范围太大
 2try:
 3    data = load_data()
 4    processed = process(data)
 5    result = analyze(processed)
 6    save(result)
 7except ValueError:
 8    print("值错误")
 9
10# 好:只包含可能出错的部分
11data = load_data()
12processed = process(data)
13
14try:
15    result = analyze(processed)
16except ValueError:
17    print("分析失败")
18
19save(result)

4. 使用with管理资源

 1# 不好:手动管理资源
 2file = open("data.txt")
 3try:
 4    content = file.read()
 5finally:
 6    file.close()
 7
 8# 好:with自动管理
 9with open("data.txt") as file:
10    content = file.read()
11# 自动关闭,即使有异常

10. 实用示例

文件读取with重试

 1import time
 2
 3def read_file_with_retry(filename, max_retries=3):
 4    """带重试的文件读取"""
 5    for attempt in range(max_retries):
 6        try:
 7            with open(filename) as f:
 8                return f.read()
 9        except FileNotFoundError:
10            if attempt < max_retries - 1:
11                print(f"文件未找到,{attempt + 1}秒后重试...")
12                time.sleep(attempt + 1)
13            else:
14                print("达到最大重试次数")
15                raise

数据验证

 1def validate_user_input(data):
 2    """验证用户输入"""
 3    errors = []
 4    
 5    try:
 6        age = int(data.get("age"))
 7        if age < 0 or age > 150:
 8            errors.append("年龄不合理")
 9    except (ValueError, TypeError):
10        errors.append("年龄必须是数字")
11    
12    if not data.get("name"):
13        errors.append("姓名不能为空")
14    
15    if errors:
16        raise ValueError(f"输入验证失败:{', '.join(errors)}")
17    
18    return True

11. 小结

今天我们学习了Python异常处理:

  • 异常vs错误:语法错误vs运行时异常
  • try-except:捕获和处理异常
  • 常见异常:ValueError、TypeError、FileNotFoundError等
  • 多异常处理:多个except、元组、Exception
  • else:无异常时执行
  • finally:总是执行,用于清理
  • raise:主动抛出异常
  • 自定义异常:继承Exception
  • 最佳实践:具体异常、记录错误、缩小范围

异常处理是写健壮程序的关键,让程序能够优雅地应对各种错误情况。


练习题

  1. 写一个函数,安全地将字符串转换为整数,处理所有可能的异常
  2. 实现一个文件复制函数,包含完整的异常处理
  3. 创建自定义异常类层次结构,用于用户认证系统

本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读