Python 错误和异常处理
目录
错误与异常概述
什么是错误?
程序运行时出现的问题分为两类:
- 语法错误(Syntax Errors):代码不符合 Python 语法规则
- 异常(Exceptions):运行时错误,语法正确但执行出错
# ❌ 语法错误 - 程序无法运行
print("Hello" # 缺少右括号
# ✅ 异常 - 程序可以运行,但会出错
print(1 / 0) # ZeroDivisionError
为什么需要异常处理?
# ❌ 没有异常处理 - 程序崩溃
def divide(a, b):
return a / b
result = divide(10, 0) # 程序崩溃!
print("这行不会执行")
# ✅ 有异常处理 - 优雅处理错误
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("除数不能为零")
return None
result = safe_divide(10, 0)
print("程序继续运行") # 这行会执行
语法错误
常见语法错误
# 1. 缺少冒号
# if True # SyntaxError
# print("Hello")
# 2. 缩进错误
# def hello():
# print("Hello") # IndentationError
# 3. 括号不匹配
# print((1 + 2) # SyntaxError
# 4. 引号不匹配
# text = "Hello' # SyntaxError
# 5. 关键字拼写错误
# fro i in range(10): # SyntaxError
# print(i)
解读语法错误信息
# 运行以下代码会产生错误信息
# print("Hello"
"""
错误信息示例:
File "example.py", line 1
print("Hello"
^
SyntaxError: unexpected EOF while parsing
解读:
- File "example.py", line 1: 错误发生在 example.py 第 1 行
- SyntaxError: 错误类型是语法错误
- unexpected EOF while parsing: 具体错误描述
- ^ 符号指向出错位置
"""
异常类型
Python 内置异常层次结构
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ImportError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── RuntimeError
├── StopIteration
├── SyntaxError
├── TypeError
├── ValueError
└── ...
常见异常类型
# 1. ZeroDivisionError - 除以零
# result = 1 / 0
# 2. TypeError - 类型错误
# result = "hello" + 5
# 3. ValueError - 值错误
# number = int("abc")
# 4. IndexError - 索引超出范围
# my_list = [1, 2, 3]
# print(my_list[10])
# 5. KeyError - 字典键不存在
# my_dict = {"a": 1}
# print(my_dict["b"])
# 6. AttributeError - 属性不存在
# my_list = [1, 2, 3]
# my_list.appendx(4)
# 7. FileNotFoundError - 文件不存在
# file = open("nonexistent.txt")
# 8. ImportError - 导入错误
# import nonexistent_module
# 9. NameError - 变量未定义
# print(undefined_variable)
# 10. AssertionError - 断言失败
# assert False, "断言失败"
查看异常信息
try:
result = 1 / 0
except ZeroDivisionError as e:
print(f"异常类型: {type(e).__name__}")
print(f"异常信息: {e}")
print(f"异常详情: {repr(e)}")
# 输出:
# 异常类型: ZeroDivisionError
# 异常信息: division by zero
# 异常详情: ZeroDivisionError('division by zero')
异常处理基础
try-except 基本语法
try:
# 可能抛出异常的代码
result = 10 / 0
except ZeroDivisionError:
# 处理特定异常
print("捕获到除零错误")
print("程序继续执行")
捕获多个异常
# 方法1:多个 except 块
try:
number = int(input("请输入数字: "))
result = 10 / number
except ValueError:
print("输入的不是有效数字")
except ZeroDivisionError:
print("不能除以零")
# 方法2:一个 except 捕获多个异常类型
try:
number = int(input("请输入数字: "))
result = 10 / number
except (ValueError, ZeroDivisionError) as e:
print(f"发生错误: {e}")
捕获所有异常
# ⚠️ 不推荐:捕获所有异常会隐藏问题
try:
# 一些操作
pass
except Exception as e:
print(f"发生未知错误: {e}")
# ✅ 推荐:只捕获预期的异常
try:
result = 10 / int(user_input)
except (ValueError, ZeroDivisionError) as e:
print(f"输入错误: {e}")
try-except 详解
完整的 try 语句结构
try:
# 可能抛出异常的代码
print("尝试执行")
except ValueError as e:
# 处理特定异常
print(f"值错误: {e}")
except TypeError as e:
# 处理另一种异常
print(f"类型错误: {e}")
else:
# 没有异常时执行
print("成功执行")
finally:
# 无论是否异常都执行
print("清理工作")
异常处理流程
def demonstrate_flow(divisor):
"""演示异常处理流程"""
try:
print("1. 进入 try 块")
result = 10 / divisor
print(f"2. 计算结果: {result}")
except ZeroDivisionError:
print("3. 捕获到 ZeroDivisionError")
result = None
except Exception as e:
print(f"3. 捕获到其他异常: {e}")
result = None
else:
print("4. 没有异常,执行 else")
finally:
print("5. 执行 finally(总是执行)")
print(f"6. 函数结束,result = {result}\n")
return result
demonstrate_flow(2) # 正常情况
demonstrate_flow(0) # 异常情况
嵌套异常处理
try:
print("外层 try")
try:
print("内层 try")
result = 1 / 0
except ZeroDivisionError:
print("内层 except: 捕获除零错误")
raise # 重新抛出异常
except ZeroDivisionError:
print("外层 except: 再次捕获除零错误")
finally:
print("外层 finally")
# 输出:
# 外层 try
# 内层 try
# 内层 except: 捕获除零错误
# 外层 except: 再次捕获除零错误
# 外层 finally
finally 子句
finally 的作用
finally 块中的代码无论是否发生异常都会执行,常用于清理资源。
# 文件操作示例
file = None
try:
file = open("test.txt", "w")
file.write("Hello, World!")
except IOError as e:
print(f"文件操作错误: {e}")
finally:
# 确保文件被关闭
if file:
file.close()
print("文件已关闭")
数据库连接示例
import sqlite3
def query_database(sql):
conn = None
cursor = None
try:
conn = sqlite3.connect("example.db")
cursor = conn.cursor()
cursor.execute(sql)
results = cursor.fetchall()
return results
except sqlite3.Error as e:
print(f"数据库错误: {e}")
return []
finally:
# 确保资源被释放
if cursor:
cursor.close()
if conn:
conn.close()
print("数据库连接已关闭")
finally 中的 return
def test_finally_return():
try:
print("try 块")
return "from try"
except:
print("except 块")
return "from except"
finally:
print("finally 块")
# ⚠️ finally 中的 return 会覆盖其他的 return
# return "from finally"
result = test_finally_return()
print(f"返回值: {result}")
# 输出:
# try 块
# finally 块
# 返回值: from try
else 子句
else 的作用
else 块在没有异常发生时执行,用于区分正常流程和异常处理。
def read_config(filename):
try:
with open(filename, 'r') as f:
config = f.read()
except FileNotFoundError:
print(f"配置文件 {filename} 不存在")
return {}
else:
# 只有在文件成功读取时才执行
print("配置文件读取成功")
return parse_config(config)
def parse_config(content):
"""解析配置内容"""
# 模拟解析
return {"key": "value"}
else vs 直接在 try 中
# ❌ 不推荐:异常处理范围过大
try:
file = open("data.txt")
data = file.read()
result = process_data(data) # 如果这里出错,会被误认为是文件错误
file.close()
except IOError:
print("IO 错误")
# ✅ 推荐:使用 else 缩小异常处理范围
try:
file = open("data.txt")
except IOError:
print("文件打开失败")
else:
try:
data = file.read()
result = process_data(data)
finally:
file.close()
抛出异常
raise 语句
# 1. 抛出内置异常
def set_age(age):
if age < 0 or age > 150:
raise ValueError(f"无效的年龄: {age}")
return age
try:
set_age(-5)
except ValueError as e:
print(e) # 无效的年龄: -5
带消息的异常
# 2. 抛出自定义消息
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("取款金额必须大于零")
if amount > balance:
raise ValueError(f"余额不足: 余额 {balance}, 请求 {amount}")
return balance - amount
try:
withdraw(100, 150)
except ValueError as e:
print(f"错误: {e}")
重新抛出异常
def process_data(data):
try:
result = int(data)
except ValueError as e:
print(f"转换失败: {e}")
raise # 重新抛出原始异常
try:
process_data("abc")
except ValueError:
print("捕获到重新抛出的异常")
异常转换
def read_config_file(filename):
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError as e:
# 转换为更具体的业务异常
raise ConfigError(f"配置文件 {filename} 不存在") from e
class ConfigError(Exception):
"""配置错误"""
pass
自定义异常
创建自定义异常类
# 1. 基本自定义异常
class MyError(Exception):
"""我的自定义异常"""
pass
raise MyError("出错了")
带属性的自定义异常
class ValidationError(Exception):
"""验证错误"""
def __init__(self, field, message, value=None):
self.field = field
self.message = message
self.value = value
super().__init__(f"{field}: {message} (值: {value})")
# 使用
try:
raise ValidationError("email", "邮箱格式不正确", "invalid@email")
except ValidationError as e:
print(f"字段: {e.field}")
print(f"消息: {e.message}")
print(f"值: {e.value}")
print(f"完整信息: {e}")
异常层次结构
# 创建异常层次结构
class AppError(Exception):
"""应用基础异常"""
pass
class DatabaseError(AppError):
"""数据库错误"""
pass
class ConnectionError(DatabaseError):
"""连接错误"""
pass
class QueryError(DatabaseError):
"""查询错误"""
pass
class BusinessError(AppError):
"""业务逻辑错误"""
pass
class InsufficientFundsError(BusinessError):
"""余额不足"""
pass
# 使用:可以捕获特定异常或父类异常
try:
raise InsufficientFundsError("余额不足")
except BusinessError as e:
print(f"业务错误: {e}")
except AppError as e:
print(f"应用错误: {e}")
实际示例:用户注册验证
class RegistrationError(Exception):
"""注册错误基类"""
pass
class UsernameError(RegistrationError):
"""用户名错误"""
pass
class EmailError(RegistrationError):
"""邮箱错误"""
pass
class PasswordError(RegistrationError):
"""密码错误"""
pass
def validate_username(username):
if not username:
raise UsernameError("用户名不能为空", username)
if len(username) < 3:
raise UsernameError("用户名至少3个字符", username)
if not username.isalnum():
raise UsernameError("用户名只能包含字母和数字", username)
return True
def validate_email(email):
if not email:
raise EmailError("邮箱不能为空", email)
if "@" not in email or "." not in email:
raise EmailError("邮箱格式不正确", email)
return True
def validate_password(password):
if not password:
raise PasswordError("密码不能为空", password)
if len(password) < 8:
raise PasswordError("密码至少8个字符", password)
if not any(c.isdigit() for c in password):
raise PasswordError("密码必须包含数字", password)
if not any(c.isalpha() for c in password):
raise PasswordError("密码必须包含字母", password)
return True
def register_user(username, email, password):
"""注册用户"""
try:
validate_username(username)
validate_email(email)
validate_password(password)
print(f"✓ 用户 {username} 注册成功")
return True
except RegistrationError as e:
print(f"✗ 注册失败: {e}")
return False
# 测试
register_user("ab", "test@example.com", "password123") # 用户名太短
register_user("john", "invalid", "password123") # 邮箱格式错误
register_user("john", "john@example.com", "12345678") # 密码无字母
register_user("john", "john@example.com", "Password1") # 成功
异常链
from 关键字
# 异常链:保留原始异常信息
def process_file(filename):
try:
with open(filename, 'r') as f:
data = f.read()
return int(data)
except FileNotFoundError as e:
raise ValueError(f"无法处理文件 {filename}") from e
except ValueError as e:
raise ValueError(f"文件内容无效: {filename}") from e
try:
process_file("nonexistent.txt")
except ValueError as e:
print(f"错误: {e}")
print(f"原因: {e.__cause__}")
# 输出:
# 错误: 无法处理文件 nonexistent.txt
# 原因: [Errno 2] No such file or directory: 'nonexistent.txt'
抑制异常链
# 使用 from None 抑制异常链
def convert_to_int(value):
try:
return int(value)
except ValueError as e:
raise TypeError("必须是数字字符串") from None
try:
convert_to_int("abc")
except TypeError as e:
print(e) # 必须是数字字符串(不显示原始异常)
上下文管理器
with 语句
with 语句自动管理资源的获取和释放,即使发生异常也能正确清理。
# 文件操作
with open("test.txt", "w") as f:
f.write("Hello, World!")
# 文件自动关闭,即使发生异常
# 多个上下文管理器
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
data = infile.read()
outfile.write(data.upper())
创建自定义上下文管理器
方法1:类实现
class ManagedResource:
"""自定义上下文管理器"""
def __init__(self, name):
self.name = name
def __enter__(self):
"""进入上下文时调用"""
print(f"打开资源: {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文时调用"""
print(f"关闭资源: {self.name}")
# 如果返回 True,异常会被抑制
# 如果返回 False 或 None,异常会继续传播
if exc_type is not None:
print(f"发生异常: {exc_type.__name__}: {exc_val}")
return False # 不抑制异常
# 使用
with ManagedResource("数据库连接") as resource:
print("使用资源")
# raise ValueError("测试异常")
# 输出:
# 打开资源: 数据库连接
# 使用资源
# 关闭资源: 数据库连接
方法2:生成器实现
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
"""使用生成器创建上下文管理器"""
print(f"打开资源: {name}")
try:
yield name
finally:
print(f"关闭资源: {name}")
# 使用
with managed_resource("网络连接") as resource:
print(f"使用资源: {resource}")
实际应用:计时器
import time
from contextlib import contextmanager
@contextmanager
def timer(description="操作"):
"""计时代码执行时间"""
start = time.time()
try:
yield
finally:
end = time.time()
elapsed = end - start
print(f"{description} 耗时: {elapsed:.4f} 秒")
# 使用
with timer("数据处理"):
time.sleep(1) # 模拟耗时操作
result = sum(range(1000000))
with timer("文件读取"):
with open("/etc/hosts", "r") as f:
content = f.read()
实际应用:临时目录
import os
import shutil
import tempfile
from contextlib import contextmanager
@contextmanager
def temporary_directory():
"""创建临时目录,使用后自动删除"""
temp_dir = tempfile.mkdtemp()
try:
yield temp_dir
finally:
shutil.rmtree(temp_dir)
print(f"临时目录 {temp_dir} 已删除")
# 使用
with temporary_directory() as temp_dir:
temp_file = os.path.join(temp_dir, "test.txt")
with open(temp_file, "w") as f:
f.write("临时数据")
print(f"临时文件: {temp_file}")
print(f"文件存在: {os.path.exists(temp_file)}")
print(f"退出后文件存在: {os.path.exists(temp_file)}") # False
日志记录
基本日志记录
import logging
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_data(data):
logger.info(f"开始处理数据: {data}")
try:
result = int(data) * 2
logger.debug(f"处理结果: {result}")
return result
except ValueError as e:
logger.error(f"数据处理失败: {e}", exc_info=True)
return None
process_data("10") # 正常
process_data("abc") # 异常
日志级别
import logging
logger = logging.getLogger(__name__)
# 日志级别(从低到高)
logger.debug("调试信息") # 详细信息,用于调试
logger.info("一般信息") # 确认一切正常
logger.warning("警告信息") # 意外情况,但程序仍能运行
logger.error("错误信息") # 严重问题,某些功能无法执行
logger.critical("严重错误") # 程序可能无法继续运行
记录异常堆栈
import logging
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)
def risky_operation():
try:
result = 1 / 0
except ZeroDivisionError:
# 方法1:记录异常信息
logger.exception("发生除零错误") # 自动包含堆栈跟踪
# 方法2:手动指定
# logger.error("发生除零错误", exc_info=True)
risky_operation()
# 输出包含完整的堆栈跟踪信息
日志到文件
import logging
# 创建 logger
logger = logging.getLogger("app_logger")
logger.setLevel(logging.DEBUG)
# 文件处理器
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.WARNING)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 格式化器
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 使用
logger.info("这是一般信息")
logger.warning("这是警告信息")
logger.error("这是错误信息")
最佳实践
1. 精确捕获异常
# ❌ 不推荐:捕获所有异常
try:
do_something()
except:
pass
# ❌ 不推荐:捕获过于宽泛
try:
do_something()
except Exception:
pass
# ✅ 推荐:捕获特定异常
try:
do_something()
except (ValueError, TypeError) as e:
handle_error(e)
2. 不要静默异常
# ❌ 危险:静默异常
try:
save_data()
except:
pass # 错误被吞掉,难以调试
# ✅ 推荐:至少记录日志
try:
save_data()
except IOError as e:
logging.error(f"保存数据失败: {e}")
raise # 或者采取其他措施
3. 清理资源使用 finally 或 with
# ❌ 不推荐:手动管理资源
file = open("data.txt")
try:
data = file.read()
finally:
file.close()
# ✅ 推荐:使用 with 语句
with open("data.txt") as file:
data = file.read()
4. 使用有意义的异常消息
# ❌ 不推荐:模糊的错误消息
raise ValueError("错误")
# ✅ 推荐:详细的错误消息
raise ValueError(f"用户年龄必须在 0-150 之间,收到: {age}")
5. 不要过度使用异常
# ❌ 不推荐:用异常控制流程
def find_item(items, target):
for item in items:
try:
if item == target:
return item
except:
continue
return None
# ✅ 推荐:正常的条件判断
def find_item(items, target):
for item in items:
if item == target:
return item
return None
6. EAFP vs LBYL
# EAFP (Easier to Ask Forgiveness than Permission)
# 先尝试,出错再处理(Python 风格)
try:
value = my_dict[key]
except KeyError:
value = default_value
# LBYL (Look Before You Leap)
# 先检查,再执行
if key in my_dict:
value = my_dict[key]
else:
value = default_value
# Python 更推崇 EAFP 风格
7. 自定义异常的最佳实践
# ✅ 推荐:创建异常层次结构
class MyAppError(Exception):
"""应用基础异常"""
def __init__(self, message, code=None):
super().__init__(message)
self.code = code
class ValidationError(MyAppError):
"""验证错误"""
pass
class AuthenticationError(MyAppError):
"""认证错误"""
pass
# 使用
try:
authenticate_user(token)
except AuthenticationError as e:
log_security_event(e)
raise
综合实战
实战1: 健壮的文件处理器
import os
import logging
from pathlib import Path
from contextlib import contextmanager
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class FileProcessorError(Exception):
"""文件处理错误基类"""
pass
class FileNotFoundError(FileProcessorError):
"""文件不存在"""
pass
class FileReadError(FileProcessorError):
"""文件读取错误"""
pass
class FileWriteError(FileProcessorError):
"""文件写入错误"""
pass
class FileProcessor:
"""健壮的文件处理器"""
def __init__(self, base_dir="."):
self.base_dir = Path(base_dir)
@contextmanager
def safe_open(self, filepath, mode='r'):
"""安全打开文件"""
full_path = self.base_dir / filepath
if mode == 'r' and not full_path.exists():
raise FileNotFoundError(f"文件不存在: {full_path}")
file_obj = None
try:
file_obj = open(full_path, mode, encoding='utf-8')
yield file_obj
except IOError as e:
raise FileReadError(f"文件操作失败: {full_path}, 错误: {e}") from e
finally:
if file_obj:
file_obj.close()
logger.debug(f"文件已关闭: {full_path}")
def read_file(self, filepath):
"""读取文件内容"""
try:
with self.safe_open(filepath, 'r') as f:
content = f.read()
logger.info(f"成功读取文件: {filepath}")
return content
except FileNotFoundError:
logger.warning(f"文件不存在: {filepath}")
return None
except FileReadError as e:
logger.error(f"读取文件失败: {e}")
return None
def write_file(self, filepath, content):
"""写入文件内容"""
try:
# 确保目录存在
full_path = self.base_dir / filepath
full_path.parent.mkdir(parents=True, exist_ok=True)
with self.safe_open(filepath, 'w') as f:
f.write(content)
logger.info(f"成功写入文件: {filepath}")
return True
except FileWriteError as e:
logger.error(f"写入文件失败: {e}")
return False
def append_file(self, filepath, content):
"""追加文件内容"""
try:
with self.safe_open(filepath, 'a') as f:
f.write(content)
logger.info(f"成功追加到文件: {filepath}")
return True
except FileWriteError as e:
logger.error(f"追加文件失败: {e}")
return False
def process_lines(self, filepath, callback):
"""逐行处理文件"""
results = []
line_num = 0
try:
with self.safe_open(filepath, 'r') as f:
for line in f:
line_num += 1
try:
result = callback(line.strip(), line_num)
results.append(result)
except Exception as e:
logger.warning(f"处理第 {line_num} 行失败: {e}")
results.append(None)
logger.info(f"处理完成: {filepath}, 共 {line_num} 行")
return results
except FileNotFoundError:
logger.error(f"文件不存在: {filepath}")
return []
except FileReadError as e:
logger.error(f"读取文件失败: {e}")
return []
# 使用示例
def main():
processor = FileProcessor("./data")
# 写入文件
processor.write_file("test.txt", "Hello\nWorld\nPython\n")
# 读取文件
content = processor.read_file("test.txt")
if content:
print(f"文件内容:\n{content}")
# 逐行处理
def uppercase_line(line, line_num):
return f"第{line_num}行: {line.upper()}"
results = processor.process_lines("test.txt", uppercase_line)
for result in results:
if result:
print(result)
# 处理不存在的文件
content = processor.read_file("nonexistent.txt")
print(f"不存在的文件: {content}")
if __name__ == "__main__":
main()
实战2: API 请求封装
import requests
import time
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class APIError(Exception):
"""API 错误基类"""
def __init__(self, message, status_code=None, response=None):
super().__init__(message)
self.status_code = status_code
self.response = response
class NetworkError(APIError):
"""网络错误"""
pass
class AuthenticationError(APIError):
"""认证错误"""
pass
class RateLimitError(APIError):
"""速率限制错误"""
def __init__(self, message, retry_after=None):
super().__init__(message)
self.retry_after = retry_after
class NotFoundError(APIError):
"""资源不存在"""
pass
def retry_on_failure(max_retries=3, delay=1, backoff=2):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_retries + 1):
try:
return func(*args, **kwargs)
except (NetworkError, RateLimitError) as e:
last_exception = e
if attempt < max_retries:
wait_time = delay * (backoff ** (attempt - 1))
logger.warning(
f"尝试 {attempt}/{max_retries} 失败: {e}. "
f"{wait_time} 秒后重试..."
)
time.sleep(wait_time)
else:
logger.error(f"所有 {max_retries} 次尝试都失败了")
raise last_exception
return wrapper
return decorator
class APIClient:
"""健壮的 API 客户端"""
def __init__(self, base_url, api_key=None, timeout=10):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.timeout = timeout
self.session = requests.Session()
if api_key:
self.session.headers.update({
'Authorization': f'Bearer {api_key}'
})
def _build_url(self, endpoint):
"""构建完整 URL"""
return f"{self.base_url}/{endpoint.lstrip('/')}"
def _handle_response(self, response):
"""处理响应"""
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
raise AuthenticationError(
"认证失败",
status_code=401,
response=response
)
elif response.status_code == 404:
raise NotFoundError(
f"资源不存在: {response.url}",
status_code=404,
response=response
)
elif response.status_code == 429:
retry_after = response.headers.get('Retry-After', 1)
raise RateLimitError(
"请求过于频繁",
retry_after=int(retry_after)
)
elif response.status_code >= 500:
raise NetworkError(
f"服务器错误: {response.status_code}",
status_code=response.status_code,
response=response
)
else:
raise APIError(
f"HTTP {response.status_code}: {response.text}",
status_code=response.status_code,
response=response
)
@retry_on_failure(max_retries=3, delay=1, backoff=2)
def get(self, endpoint, params=None):
"""GET 请求"""
url = self._build_url(endpoint)
try:
logger.info(f"GET {url}")
response = self.session.get(
url,
params=params,
timeout=self.timeout
)
response.raise_for_status()
return self._handle_response(response)
except requests.exceptions.ConnectionError as e:
raise NetworkError(f"连接失败: {e}") from e
except requests.exceptions.Timeout as e:
raise NetworkError(f"请求超时: {e}") from e
except requests.exceptions.RequestException as e:
raise APIError(f"请求失败: {e}") from e
@retry_on_failure(max_retries=3, delay=1, backoff=2)
def post(self, endpoint, data=None, json=None):
"""POST 请求"""
url = self._build_url(endpoint)
try:
logger.info(f"POST {url}")
response = self.session.post(
url,
data=data,
json=json,
timeout=self.timeout
)
response.raise_for_status()
return self._handle_response(response)
except requests.exceptions.RequestException as e:
raise APIError(f"POST 请求失败: {e}") from e
def close(self):
"""关闭会话"""
self.session.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
# 使用示例
def main():
# 使用 JSONPlaceholder 测试 API
with APIClient("https://jsonplaceholder.typicode.com") as client:
try:
# 获取帖子
posts = client.get("/posts", params={"_limit": 5})
print(f"获取到 {len(posts)} 个帖子")
for post in posts[:2]:
print(f" - {post['title'][:50]}...")
# 获取单个帖子
post = client.get("/posts/1")
print(f"\n帖子标题: {post['title']}")
# 尝试获取不存在的资源
try:
client.get("/posts/999999")
except NotFoundError as e:
print(f"\n预期错误: {e}")
# 发布新帖子
new_post = {
"title": "Test Post",
"body": "This is a test",
"userId": 1
}
result = client.post("/posts", json=new_post)
print(f"\n创建帖子 ID: {result['id']}")
except AuthenticationError as e:
print(f"认证错误: {e}")
except NetworkError as e:
print(f"网络错误: {e}")
except APIError as e:
print(f"API 错误: {e}")
if __name__ == "__main__":
main()
实战3: 数据验证框架
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field
import re
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class ValidationError:
"""验证错误"""
field: str
message: str
value: Any = None
def __str__(self):
return f"{self.field}: {self.message} (值: {self.value})"
@dataclass
class ValidationResult:
"""验证结果"""
is_valid: bool = True
errors: List[ValidationError] = field(default_factory=list)
def add_error(self, field: str, message: str, value: Any = None):
self.is_valid = False
self.errors.append(ValidationError(field, message, value))
def merge(self, other: 'ValidationResult'):
"""合并验证结果"""
if not other.is_valid:
self.is_valid = False
self.errors.extend(other.errors)
def raise_if_invalid(self):
"""如果验证失败,抛出异常"""
if not self.is_valid:
error_messages = "\n".join(str(e) for e in self.errors)
raise ValueError(f"验证失败:\n{error_messages}")
class Validator:
"""验证器基类"""
def validate(self, value: Any) -> ValidationResult:
raise NotImplementedError
class RequiredValidator(Validator):
"""必填验证"""
def __init__(self, message: str = "此字段为必填项"):
self.message = message
def validate(self, value: Any) -> ValidationResult:
result = ValidationResult()
if value is None or value == "":
result.add_error("field", self.message, value)
return result
class StringValidator(Validator):
"""字符串验证"""
def __init__(
self,
min_length: int = None,
max_length: int = None,
pattern: str = None,
message: str = None
):
self.min_length = min_length
self.max_length = max_length
self.pattern = re.compile(pattern) if pattern else None
self.message = message or "字符串验证失败"
def validate(self, value: Any) -> ValidationResult:
result = ValidationResult()
if not isinstance(value, str):
result.add_error("field", "必须是字符串", value)
return result
if self.min_length and len(value) < self.min_length:
result.add_error(
"field",
f"长度不能少于 {self.min_length} 个字符",
value
)
if self.max_length and len(value) > self.max_length:
result.add_error(
"field",
f"长度不能超过 {self.max_length} 个字符",
value
)
if self.pattern and not self.pattern.match(value):
result.add_error("field", self.message, value)
return result
class NumberValidator(Validator):
"""数字验证"""
def __init__(
self,
min_value: float = None,
max_value: float = None,
message: str = None
):
self.min_value = min_value
self.max_value = max_value
self.message = message or "数字验证失败"
def validate(self, value: Any) -> ValidationResult:
result = ValidationResult()
if not isinstance(value, (int, float)):
result.add_error("field", "必须是数字", value)
return result
if self.min_value is not None and value < self.min_value:
result.add_error(
"field",
f"不能小于 {self.min_value}",
value
)
if self.max_value is not None and value > self.max_value:
result.add_error(
"field",
f"不能大于 {self.max_value}",
value
)
return result
class EmailValidator(Validator):
"""邮箱验证"""
def __init__(self, message: str = "邮箱格式不正确"):
self.pattern = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
self.message = message
def validate(self, value: Any) -> ValidationResult:
result = ValidationResult()
if not isinstance(value, str):
result.add_error("field", "必须是字符串", value)
return result
if not self.pattern.match(value):
result.add_error("field", self.message, value)
return result
class FieldValidator:
"""字段验证器"""
def __init__(self, field_name: str):
self.field_name = field_name
self.validators: List[Validator] = []
def add_validator(self, validator: Validator):
self.validators.append(validator)
return self
def required(self, message: str = "此字段为必填项"):
return self.add_validator(RequiredValidator(message))
def string(self, min_length=None, max_length=None, pattern=None, message=None):
return self.add_validator(
StringValidator(min_length, max_length, pattern, message)
)
def number(self, min_value=None, max_value=None, message=None):
return self.add_validator(
NumberValidator(min_value, max_value, message)
)
def email(self, message: str = "邮箱格式不正确"):
return self.add_validator(EmailValidator(message))
def validate(self, value: Any) -> ValidationResult:
result = ValidationResult()
for validator in self.validators:
field_result = validator.validate(value)
# 更新错误信息的字段名
for error in field_result.errors:
error.field = self.field_name
result.merge(field_result)
# 如果当前验证失败,跳过后续验证
if not field_result.is_valid:
break
return result
class SchemaValidator:
"""模式验证器"""
def __init__(self):
self.fields: Dict[str, FieldValidator] = {}
def field(self, field_name: str) -> FieldValidator:
"""定义字段验证器"""
if field_name not in self.fields:
self.fields[field_name] = FieldValidator(field_name)
return self.fields[field_name]
def validate(self, data: Dict[str, Any]) -> ValidationResult:
"""验证数据"""
result = ValidationResult()
for field_name, field_validator in self.fields.items():
value = data.get(field_name)
field_result = field_validator.validate(value)
result.merge(field_result)
return result
# 使用示例
def main():
# 定义用户注册验证模式
user_schema = SchemaValidator()
user_schema.field("username") \
.required("用户名不能为空") \
.string(min_length=3, max_length=20, message="用户名只能包含字母、数字和下划线")
user_schema.field("email") \
.required("邮箱不能为空") \
.email("邮箱格式不正确")
user_schema.field("age") \
.required("年龄不能为空") \
.number(min_value=0, max_value=150, message="年龄必须在 0-150 之间")
user_schema.field("password") \
.required("密码不能为空") \
.string(min_length=8, message="密码至少8个字符")
# 测试数据
test_cases = [
{
"username": "john",
"email": "john@example.com",
"age": 25,
"password": "Password123"
},
{
"username": "ab", # 太短
"email": "invalid", # 格式错误
"age": -5, # 超出范围
"password": "123" # 太短
},
{
"username": "",
"email": "",
"age": None,
"password": ""
}
]
for i, data in enumerate(test_cases, 1):
print(f"\n{'='*60}")
print(f"测试用例 {i}:")
print(f"{'='*60}")
result = user_schema.validate(data)
if result.is_valid:
print("✓ 验证通过")
else:
print("✗ 验证失败:")
for error in result.errors:
print(f" - {error}")
if __name__ == "__main__":
main()
小结
| 概念 | 说明 | 使用场景 |
|---|---|---|
| try-except | 捕获和处理异常 | 处理可预见的错误 |
| finally | 无论如何都执行 | 清理资源 |
| else | 无异常时执行 | 区分正常和异常流程 |
| raise | 抛出异常 | 报告错误 |
| 自定义异常 | 创建特定异常类 | 业务逻辑错误 |
| 异常链 | 保留原始异常 | 异常转换时保留上下文 |
| 上下文管理器 | 自动资源管理 | 文件、数据库连接等 |
| 日志记录 | 记录错误信息 | 调试和监控 |
核心要点:
- 异常处理让程序更健壮
- 精确捕获,不要吞掉异常
- 使用 finally 或 with 清理资源
- 创建有意义的自定义异常
- 合理使用日志记录错误
- EAFP 风格更符合 Python 哲学
- 不要滥用异常控制流程
掌握异常处理是编写高质量 Python 代码的关键!