【Python从入门到精通】第 011 篇:异常处理:写出健壮的Python代码
上一篇【第 010 篇】Python模块与包:代码组织最佳实践
下一篇【第 012 篇】Python内置库精讲:os、sys、pathlib、datetime
系列说明:本系列共30篇,涵盖Python开发完整学习路径。本文为第011篇,深入讲解Python异常处理机制。
摘要
在实际的Python开发过程中,程序运行时难免会遇到各种错误:用户输入无效数据、文件不存在、网络连接中断、数组越界访问等。如果这些错误得不到妥善处理,程序就会崩溃退出,用户体验大打折扣。Python提供了完善的异常处理机制,让我们能够优雅地捕获、处理这些运行时错误,确保程序即使遇到问题也能从容应对,而不是直接崩溃。
本文将系统讲解Python异常处理的核心知识:从最基础的try-except语法开始,到else和finally子句的配合使用,再到自定义异常类和异常的链式传递。
一、异常基础
1.1 什么是异常
在Python中,异常是程序执行过程中发生的事件,它会中断正常的指令流。当Python检测到某个错误时,会创建一个异常对象。
# 除数为零的情况
result = 10 / 0
# ZeroDivisionError: division by zero
# 使用未定义的变量
print(spam)
# NameError: name 'spam' is not defined
语法错误与异常的区别:
- 语法错误(Syntax Errors):代码解析阶段就被发现的错误
- 异常(Exceptions):代码语法正确,但运行时才发生的错误
1.2 常见异常类型
| 异常类型 | 说明 | 典型场景 |
|---|---|---|
ZeroDivisionError |
除数为零 | 10 / 0 |
NameError |
名称未定义 | 使用不存在的变量 |
TypeError |
类型不匹配 | '2' + 2 |
ValueError |
值不合法 | int('abc') |
IndexError |
索引越界 | 列表只有3个元素却访问第5个 |
KeyError |
键不存在 | 字典中没有这个键 |
FileNotFoundError |
文件不存在 | 打开不存在的文件 |
1.3 异常的错误信息解读
Python的Traceback按从上到下顺序解读:
Traceback (most recent call last):
File "example.py", line 8, in <module>
main()
File "example.py", line 5, in main
print(names[10])
IndexError: list index out of range
- 最外层:程序执行的起点
- 调用链的上一级:函数调用关系
- 异常类型和消息:告诉我们发生了什么问题
二、try-except 语法
2.1 基本语法结构
def safe_divide(a, b):
"""安全的除法运算"""
try:
result = a / b
print(f"计算结果: {result}")
except ZeroDivisionError:
print("错误: 除数不能为零!")
safe_divide(10, 2) # 计算结果: 5.0
safe_divide(10, 0) # 错误: 除数不能为零!
2.2 捕获特定异常
def process_user_input():
"""处理用户输入"""
try:
age = int(input("请输入您的年龄: "))
print(f"您的年龄是: {age}")
except ValueError:
print("错误: 请输入有效的整数!")
2.3 多个except子句
def read_file(filename):
"""读取文件内容"""
try:
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
print(f"文件内容长度: {len(content)} 字符")
except FileNotFoundError:
print(f"错误: 文件 '{filename}' 不存在!")
except PermissionError:
print(f"错误: 没有权限读取文件 '{filename}'!")
2.4 捕获异常对象
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"异常类型: {type(e).__name__}")
print(f"异常消息: {e}")
2.5 异常处理顺序
应该先捕获子类,再捕获父类:
try:
risky_code()
except SpecificError:
# 先处理具体的异常
handle_specific()
except BaseError:
# 再处理更通用的异常
handle_base()
三、else与finally子句
3.1 else的用途
else块中的代码会在try块成功执行且没有发生异常时运行:
def parse_number(text):
"""解析数字"""
try:
number = int(text)
except ValueError:
print(f"'{text}' 不是有效的数字")
else:
# 只有在成功解析时才执行
print(f"成功解析数字: {number}")
return number
parse_number("42") # 成功解析数字: 42
parse_number("hello") # 'hello' 不是有效的数字
3.2 finally的用途
无论try块中是否发生异常,finally子句中的代码总是会执行:
def demonstrate_finally():
"""演示finally的作用"""
try:
result = 10 / 0
except ZeroDivisionError:
print("捕获到 ZeroDivisionError")
finally:
print("finally块总是执行")
3.3 资源清理
finally最常见的用途是资源清理:
def copy_file(source_path, dest_path):
"""复制文件"""
source_file = None
dest_file = None
try:
source_file = open(source_path, 'r', encoding='utf-8')
dest_file = open(dest_path, 'w', encoding='utf-8')
content = source_file.read()
dest_file.write(content)
except FileNotFoundError as e:
print(f"错误: {e}")
finally:
if source_file is not None:
source_file.close()
if dest_file is not None:
dest_file.close()
不过,更好的做法是使用with语句(上下文管理器)。
四、抛出异常
4.1 raise语句
def validate_age(age):
"""验证年龄是否合法"""
if age < 0:
raise ValueError("年龄不能为负数")
if age > 150:
raise ValueError("年龄值不合理")
return True
try:
validate_age(-5)
except ValueError as e:
print(f"验证失败: {e}")
raise语句的几种形式:
# 1. 抛出异常对象
raise ValueError("错误信息")
# 2. 重新抛出当前捕获的异常
except Exception:
raise
# 3. 重新抛出指定异常
except SomeError:
raise AnotherError("新的错误") from None
4.2 自定义异常类
class ValidationError(Exception):
"""数据验证错误"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class EmailValidator:
@staticmethod
def validate(email):
if '@' not in email:
raise ValidationError('email', '邮箱格式不正确')
return True
try:
EmailValidator.validate("not-an-email")
except ValidationError as e:
print(f"验证错误 - 字段: {e.field}, 原因: {e.message}")
4.3 异常的链式传递
隐式异常链:
def read_config(filename):
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
raise RuntimeError(f"无法读取配置文件: {filename}")
显式异常链(使用from):
try:
result = int("not a number")
except ValueError as original:
raise RuntimeError("数据处理失败") from original
五、异常处理最佳实践
5.1 异常捕获原则
原则一:尽可能捕获具体异常
# 推荐 - 针对具体异常
try:
result = risky_calculation()
except ZeroDivisionError:
print("除数不能为零")
except ValueError as e:
print(f"数值错误: {e}")
原则二:让异常传播
不要过度捕获异常。如果当前代码无法合理处理某个异常,应该让它传播到上层调用者。
5.2 避免裸except
# ❌ 不推荐
try:
result = dangerous_operation()
except:
print("出错了")
# ✅ 推荐
try:
result = dangerous_operation()
except Exception as e:
print(f"操作失败: {e}")
5.3 异常日志记录
import logging
logging.basicConfig(
level=logging.ERROR,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_data(data):
try:
return validate_and_transform(data)
except ValueError as e:
logger.error(f"数据验证失败: %s", data, exc_info=True)
raise
5.4 异常与用户输入验证
def get_positive_number():
"""获取正数"""
while True:
try:
number = int(input("请输入一个正数: "))
if number <= 0:
print("错误: 必须输入正数")
continue
return number
except ValueError:
print("错误: 请输入有效的整数")
六、实战:健壮的程序设计
6.1 异常处理模式
重试机制
import time
def retry_with_backoff(func, max_retries=3, initial_delay=1):
"""带退避的重试机制"""
delay = initial_delay
for attempt in range(max_retries):
try:
return func()
except (ConnectionError, TimeoutError) as e:
if attempt == max_retries - 1:
raise
print(f"第{attempt + 1}次尝试失败,{delay}秒后重试...")
time.sleep(delay)
delay *= 2
6.2 上下文管理器
with语句确保资源在使用后被正确释放:
# 文件操作
with open('example.txt', 'r', encoding='utf-8') as file:
content = file.read()
# 文件自动关闭
# 自定义上下文管理器
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.time() - self.start
print(f"耗时 {elapsed:.2f} 秒")
return False
with Timer() as t:
data = list(range(100000))
data.sort()
6.3 断言的使用
断言用于在开发阶段检查程序状态:
def calculate_average(numbers):
"""计算平均值"""
assert len(numbers) > 0, "数字列表不能为空"
return sum(numbers) / len(numbers)
# 断言与异常的区别
def factorial(n):
assert n >= 0, "n必须是非负整数" # 开发阶段检查
if n == 0:
return 1
return n * factorial(n - 1)
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零") # 用户输入需要运行时检查
return a / b
七、常见问题与注意事项
Q1: 异常处理会影响性能吗?
正常情况下,异常处理几乎没有性能影响。只有在异常真的发生时,才会有少量额外开销。
Q2: 什么时候应该捕获异常?
- 当你知道如何处理这个异常时
- 当你需要执行某些清理操作时
- 当你想要提供更友好的错误信息时
Q3: 什么时候应该让异常传播?
- 当当前代码无法合理处理这个异常时
- 当这个异常应该终止程序时(如配置错误)
八、总结
本文详细介绍了Python异常处理的核心知识:
- 异常基础:异常是程序运行时发生的错误
- try-except语法:捕获并处理特定异常
- else与finally:
else块在成功时运行,finally块总是执行 - 抛出异常:使用
raise语句主动抛出异常 - 最佳实践:尽量捕获具体异常,使用日志记录异常
上一篇【第 010 篇】Python模块与包:代码组织最佳实践
下一篇【第 012 篇】Python内置库精讲:os、sys、pathlib、datetime
参考资料
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)