Python异常处理:try-except让程序不再动不动就崩溃
异常处理是编写健壮程序的关键。Python使用try-except机制来捕获和处理运行时错误,使程序能够优雅地处理意外情况而不是直接崩溃。本篇将详细介绍异常的概念、处理方法和最佳实践。
1. 什么是异常
异常是程序运行时发生的错误,会中断程序的正常执行流程。
# 常见的异常示例
print(10 / 0) # ZeroDivisionError: division by zero
print(int("abc")) # ValueError: invalid literal for int()
print(my_list[100]) # IndexError: list index out of range
print(my_dict["key"]) # KeyError: 'key'
open("不存在.txt") # FileNotFoundError
# 异常与错误的区别
# 语法错误(SyntaxError):代码不符合Python语法,无法运行
# 异常(Exception):代码语法正确,但运行时出错
与C语言的对比:
| 特性 | C语言 | Python |
|---|---|---|
| 错误处理 | 返回错误码、errno | 异常机制 |
| 处理方式 | 检查返回值 | try-except |
| 传播方式 | 手动传递 | 自动向上传播 |
2. 异常处理语法
2.1 基本的try-except
try:
# 可能出错的代码
result = 10 / 0
except:
# 处理异常
print("发生了错误")
# 不推荐捕获所有异常,应该捕获特定异常
2.2 捕获特定异常
try:
num = int(input("请输入数字:"))
result = 10 / num
print(f"结果:{result}")
except ValueError:
print("输入的不是有效数字")
except ZeroDivisionError:
print("除数不能为零")
2.3 捕获多个异常
# 方式1:多个except块
try:
# 代码
pass
except ValueError:
print("值错误")
except TypeError:
print("类型错误")
except Exception as e:
print(f"其他错误:{e}")
# 方式2:元组形式
try:
# 代码
pass
except (ValueError, TypeError) as e:
print(f"值或类型错误:{e}")
2.4 else子句
else块在没有异常发生时执行。
try:
num = int(input("请输入数字:"))
except ValueError:
print("输入无效")
else:
# 没有异常时执行
print(f"你输入的是:{num}")
print(f"它的平方是:{num ** 2}")
2.5 finally子句
finally块无论是否发生异常都会执行,常用于清理资源。
try:
f = open("file.txt", "r")
content = f.read()
except FileNotFoundError:
print("文件不存在")
finally:
# 无论如何都会执行
print("清理资源")
if 'f' in locals() and not f.closed:
f.close()
# 完整的try语句
try:
# 尝试执行的代码
result = risky_operation()
except SpecificError:
# 处理特定异常
handle_error()
else:
# 没有异常时执行
process_result(result)
finally:
# 总是执行(清理)
cleanup()
3. 常见内置异常
| 异常 | 说明 | 示例 |
|---|---|---|
Exception | 所有异常的基类 | - |
ValueError | 值错误 | int("abc") |
TypeError | 类型错误 | "2" + 2 |
IndexError | 索引越界 | lst[100] |
KeyError | 字典键不存在 | d["missing"] |
FileNotFoundError | 文件不存在 | open("no.txt") |
ZeroDivisionError | 除零错误 | 1 / 0 |
AttributeError | 属性不存在 | "str".foo() |
ImportError | 导入失败 | import xxx |
NameError | 名称未定义 | print(undefined) |
RuntimeError | 运行时错误 | - |
StopIteration | 迭代器耗尽 | next(empty_iter) |
PermissionError | 权限错误 | 无权限访问文件 |
TimeoutError | 超时错误 | 网络超时 |
# 异常层次结构(部分)
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# ├── GeneratorExit
# └── Exception
# ├── ValueError
# ├── TypeError
# ├── IndexError
# ├── KeyError
# ├── FileNotFoundError
# ├── ZeroDivisionError
# └── ...
4. 异常信息获取
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"异常类型:{type(e).__name__}")
print(f"异常信息:{e}")
print(f"异常参数:{e.args}")
# 获取完整的堆栈信息
import traceback
try:
result = 10 / 0
except ZeroDivisionError:
# 打印堆栈信息
traceback.print_exc()
# 获取堆栈信息字符串
error_info = traceback.format_exc()
print(error_info)
# 使用logging记录异常
import logging
logging.basicConfig(level=logging.ERROR)
try:
result = 10 / 0
except ZeroDivisionError:
logging.exception("发生除零错误") # 自动包含堆栈信息
5. 抛出异常
# 使用raise抛出异常
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
try:
result = divide(10, 0)
except ValueError as e:
print(e)
# 重新抛出异常
try:
result = 10 / 0
except ZeroDivisionError:
print("记录错误...")
raise # 重新抛出当前异常
# 抛出带有原因的异常
try:
result = int("abc")
except ValueError as original:
raise RuntimeError("数据处理失败") from original
# 条件性抛出
def validate_age(age):
if not isinstance(age, int):
raise TypeError("年龄必须是整数")
if age < 0 or age > 150:
raise ValueError("年龄必须在0-150之间")
return True
6. 自定义异常
# 简单的自定义异常
class MyError(Exception):
pass
# 带有额外信息的自定义异常
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
# 使用自定义异常
def validate_user(data):
if not data.get("name"):
raise ValidationError("name", "姓名不能为空")
if not data.get("email"):
raise ValidationError("email", "邮箱不能为空")
if "@" not in data.get("email", ""):
raise ValidationError("email", "邮箱格式不正确")
try:
validate_user({"name": "张三"})
except ValidationError as e:
print(f"验证失败 - 字段:{e.field},原因:{e.message}")
# 异常层次结构
class AppError(Exception):
"""应用程序基础异常"""
pass
class DatabaseError(AppError):
"""数据库相关异常"""
pass
class NetworkError(AppError):
"""网络相关异常"""
pass
class ConfigError(AppError):
"""配置相关异常"""
pass
7. 异常链
# 异常链:保留原始异常信息
def read_config(filename):
try:
with open(filename) as f:
return f.read()
except FileNotFoundError as e:
raise ConfigError(f"配置文件不存在:{filename}") from e
# 显式禁用异常链
try:
result = int("abc")
except ValueError:
raise RuntimeError("处理失败") from None # 不显示原始异常
# 隐式异常链(在except块中抛出新异常)
try:
try:
result = 10 / 0
except ZeroDivisionError:
raise ValueError("计算错误") # 隐式链接到ZeroDivisionError
except ValueError as e:
print(f"异常:{e}")
print(f"原因:{e.__cause__}") # 显式链接的原因
print(f"上下文:{e.__context__}") # 隐式链接的上下文
8. 上下文管理器与异常
# with语句自动处理异常和资源清理
with open("file.txt") as f:
content = f.read()
# 即使发生异常,文件也会被正确关闭
# 自定义上下文管理器
class DatabaseConnection:
def __init__(self, host):
self.host = host
self.connection = None
def __enter__(self):
print(f"连接到 {self.host}")
self.connection = "connected"
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("关闭连接")
self.connection = None
# 返回True表示异常已处理,不再传播
# 返回False或None表示异常继续传播
if exc_type is ValueError:
print("处理了ValueError")
return True
return False
with DatabaseConnection("localhost") as db:
print("执行数据库操作")
# raise ValueError("测试异常")
# 使用contextlib简化
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"获取资源:{name}")
try:
yield name
finally:
print(f"释放资源:{name}")
with managed_resource("文件") as r:
print(f"使用资源:{r}")
9. 调试技巧
# 1. 使用assert进行调试断言
def calculate_average(numbers):
assert len(numbers) > 0, "列表不能为空"
assert all(isinstance(n, (int, float)) for n in numbers), "必须是数字"
return sum(numbers) / len(numbers)
# 注意:assert可以被禁用(python -O)
# 2. 使用pdb调试器
import pdb
def buggy_function(x):
result = x * 2
pdb.set_trace() # 在此处暂停,进入调试模式
result = result + 10
return result
# pdb常用命令:
# n (next): 执行下一行
# s (step): 进入函数
# c (continue): 继续执行
# p variable: 打印变量
# l (list): 显示代码
# q (quit): 退出
# 3. 使用breakpoint()(Python 3.7+)
def another_function(x):
breakpoint() # 等同于pdb.set_trace()
return x * 2
# 4. 打印调试信息
def debug_print(*args, **kwargs):
import sys
print(*args, file=sys.stderr, **kwargs)
# 5. 使用logging
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def process_data(data):
logging.debug(f"输入数据:{data}")
result = data * 2
logging.debug(f"处理结果:{result}")
return result
10. 最佳实践
✅ 推荐做法
# 1. 捕获特定异常,而不是所有异常
try:
value = int(user_input)
except ValueError:
print("请输入有效数字")
# 2. 使用else分离正常逻辑
try:
f = open("file.txt")
except FileNotFoundError:
print("文件不存在")
else:
content = f.read()
f.close()
# 3. 使用finally确保资源清理
# 或更好的方式:使用with语句
# 4. 提供有意义的错误信息
if not data:
raise ValueError("数据不能为空,请检查输入")
# 5. 记录异常信息
import logging
try:
risky_operation()
except Exception as e:
logging.exception("操作失败")
raise
# 6. 使用自定义异常表达业务逻辑
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足:当前{balance},需要{amount}")
❌ 避免的做法
# 1. 不要捕获所有异常然后忽略
try:
risky_operation()
except:
pass # 危险!隐藏了所有错误
# 2. 不要使用异常控制正常流程
# 错误
try:
value = my_dict[key]
except KeyError:
value = default
# 正确
value = my_dict.get(key, default)
# 3. 不要捕获BaseException
# 这会捕获KeyboardInterrupt和SystemExit
try:
operation()
except BaseException: # 不推荐
pass
# 4. 不要在except块中引入新的bug
try:
result = process(data)
except ValueError:
result = process(None) # 可能引发新的异常
11. 实战练习
练习1:安全的文件读取
"""
练习:实现一个安全的文件读取函数
"""
from pathlib import Path
def safe_read_file(filepath, default=None, encoding='utf-8'):
"""
安全地读取文件内容
Args:
filepath: 文件路径
default: 读取失败时的默认值
encoding: 文件编码
Returns:
文件内容或默认值
"""
try:
path = Path(filepath)
if not path.exists():
print(f"文件不存在:{filepath}")
return default
return path.read_text(encoding=encoding)
except PermissionError:
print(f"没有权限读取文件:{filepath}")
return default
except UnicodeDecodeError:
print(f"文件编码错误:{filepath}")
return default
except Exception as e:
print(f"读取文件失败:{e}")
return default
# 测试
content = safe_read_file("test.txt", default="")
print(content)
练习2:带重试的网络请求
"""
练习:实现带重试机制的函数装饰器
"""
import time
from functools import wraps
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""
重试装饰器
Args:
max_attempts: 最大尝试次数
delay: 重试间隔(秒)
exceptions: 需要重试的异常类型
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
print(f"尝试 {attempt}/{max_attempts} 失败:{e}")
if attempt < max_attempts:
print(f"等待 {delay} 秒后重试...")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
# 使用示例
@retry(max_attempts=3, delay=0.5, exceptions=(ValueError, ConnectionError))
def unreliable_function():
import random
if random.random() < 0.7:
raise ValueError("随机失败")
return "成功"
# result = unreliable_function()
练习3:输入验证
"""
练习:实现用户输入验证
"""
class InputValidator:
"""输入验证器"""
@staticmethod
def get_int(prompt, min_val=None, max_val=None):
"""获取整数输入"""
while True:
try:
value = int(input(prompt))
if min_val is not None and value < min_val:
print(f"值不能小于 {min_val}")
continue
if max_val is not None and value > max_val:
print(f"值不能大于 {max_val}")
continue
return value
except ValueError:
print("请输入有效的整数")
@staticmethod
def get_float(prompt, min_val=None, max_val=None):
"""获取浮点数输入"""
while True:
try:
value = float(input(prompt))
if min_val is not None and value < min_val:
print(f"值不能小于 {min_val}")
continue
if max_val is not None and value > max_val:
print(f"值不能大于 {max_val}")
continue
return value
except ValueError:
print("请输入有效的数字")
@staticmethod
def get_choice(prompt, choices):
"""获取选择输入"""
choices_str = "/".join(choices)
while True:
value = input(f"{prompt} ({choices_str}): ").strip()
if value in choices:
return value
print(f"请输入以下选项之一:{choices_str}")
# 使用示例
# validator = InputValidator()
# age = validator.get_int("请输入年龄:", min_val=0, max_val=150)
# choice = validator.get_choice("是否继续", ["y", "n"])
12. 总结
🔑 核心要点
| 知识点 | 要点 |
|---|---|
| try-except | 捕获和处理异常 |
| else | 没有异常时执行 |
| finally | 总是执行,用于清理 |
| raise | 抛出异常 |
| 自定义异常 | 继承Exception类 |
| 异常链 | 使用from保留原始异常 |
| 最佳实践 | 捕获特定异常,提供有意义的错误信息 |
✅ 学习检查清单
- 掌握try-except-else-finally语法
- 了解常见内置异常类型
- 能够抛出和自定义异常
- 理解异常链的概念
- 掌握调试技巧
- 了解异常处理的最佳实践
📖 下一步学习
掌握了异常处理后,让我们学习Python的模块与包:
常见问题 FAQ
💬 except后面要不要指定异常类型?
必须指定!裸except:会捕获所有异常包括KeyboardInterrupt和SystemExit,导致Ctrl+C都无法终止程序。至少写except Exception:。
💬 什么时候该抛异常,什么时候该返回错误码?
Python风格是用异常。C语言习惯返回-1表示错误,但Python推崇EAFP(先试再说)而不是LBYL(先检查再做)。该抛就抛,让调用者决定怎么处理。
� 系列导航
- 上一篇:10 - Python文件操作
- 当前:11 - Python异常处理
- 下一篇:12 - Python模块与包