引言
错误处理往往是可靠代码中的薄弱环节。键缺失、请求失败、函数运行时间过长等问题在实际项目中经常出现。Python内置的try-except块很有用,但它们本身并未涵盖许多实际场景。
你需要将常见的失败情况封装成小型、可重用的函数,以帮助处理带限制的重试、输入验证,以及防止代码运行时间过长的保护机制。本文将介绍五个错误处理函数,可用于网页抓取、构建应用程序编程接口(API)、处理用户数据等任务。
你可以在GitHub上找到相关代码。
使用指数退避重试失败操作
在许多项目中,API调用和网络请求经常失败。初学者的做法是尝试一次,捕获所有异常,记录日志,然后停止。更好的方法是进行重试。
这就用到了指数退避。不是用即时重试来冲击失败的服务(这只会让情况更糟),而是在每次尝试之间等待更长的时间:1秒,然后2秒,然后4秒,以此类推。
让我们构建一个实现此功能的装饰器:
import time
import functools
from typing import Callable, Type, Tuple
def retry_with_backoff(
max_attempts: int = 3,
base_delay: float = 1.0,
exponential_base: float = 2.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
"""
使用指数退避重试函数。
参数:
max_attempts: 最大重试次数
base_delay: 初始延迟秒数
exponential_base: 延迟乘数(2.0 = 每次加倍)
exceptions: 要捕获并重试的异常类型元组
"""
def decorator(func: Callable):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts - 1:
delay = base_delay * (exponential_base ** attempt)
print(f"尝试 {attempt + 1} 失败: {e}")
print(f"将在 {delay:.1f} 秒后重试...")
time.sleep(delay)
else:
print(f"所有 {max_attempts} 次尝试均失败")
raise last_exception
return wrapper
return decorator
该装饰器包装你的函数并捕获指定的异常。关键计算是 delay = base_delay * (exponential_base ** attempt)。当 base_delay=1 且 exponential_base=2 时,延迟为1秒、2秒、4秒、8秒。这给过载的系统留出了恢复时间。
exceptions 参数让你指定哪些错误需要重试。你可能重试 ConnectionError 但不会重试 ValueError,因为连接问题是暂时的,而验证错误不是。
现在看看它的实际效果:
import random
@retry_with_backoff(max_attempts=4, base_delay=0.5, exceptions=(ConnectionError,))
def fetch_user_data(user_id):
"""模拟一个不可靠的API。"""
if random.random() < 0.6: # 60% 失败率
raise ConnectionError("服务暂时不可用")
return {"id": user_id, "name": "Sara", "status": "active"}
# 观察自动重试
result = fetch_user_data(12345)
print(f"成功: {result}")
输出:
成功: {'id': 12345, 'name': 'Sara', 'status': 'active'}
使用可组合规则验证输入
用户输入验证既繁琐又重复。你需要检查字符串是否为空、数字是否在范围内、邮箱地址是否看起来有效。不知不觉中,代码里到处都是嵌套的if语句,变得一团糟。
让我们构建一个简单易用的验证系统。首先,需要一个自定义异常:
from typing import Any, Callable, Dict, List, Optional
class ValidationError(Exception):
"""验证失败时抛出。"""
def __init__(self, field: str, errors: List[str]):
self.field = field
self.errors = errors
super().__init__(f"{field}: {', '.join(errors)}")
这个异常包含多条错误信息。当验证失败时,我们希望向用户展示所有错误,而不仅仅是第一个错误。
下面是验证器:
def validate_input(
value: Any,
field_name: str,
rules: Dict[str, Callable[[Any], bool]],
messages: Optional[Dict[str, str]] = None
) -> Any:
"""
根据多个规则验证输入。
若值有效则返回值,否则抛出 ValidationError。
"""
if messages is None:
messages = {}
errors = []
for rule_name, rule_func in rules.items():
try:
if not rule_func(value):
error_msg = messages.get(
rule_name,
f"验证规则失败: {rule_name}"
)
errors.append(error_msg)
except Exception as e:
errors.append(f"{rule_name} 中的验证错误: {str(e)}")
if errors:
raise ValidationError(field_name, errors)
return value
在 rules 字典中,每个规则都是一个返回 True 或 False 的函数。这使得规则可组合且可重用。
创建一些常见的验证规则:
# 可重用的验证规则
def not_empty(value: str) -> bool:
return bool(value and value.strip())
def min_length(min_len: int) -> Callable:
return lambda value: len(str(value)) >= min_len
def max_length(max_len: int) -> Callable:
return lambda value: len(str(value)) <= max_len
def in_range(min_val: float, max_val: float) -> Callable:
return lambda value: min_val <= float(value) <= max_val
注意 min_length、max_length 和 in_range 是工厂函数。它们返回配置了特定参数的验证函数。这样你就可以写 min_length(3) 而不是为每个长度要求创建新函数。
验证一个用户名:
try:
username = validate_input(
"ab",
"username",
{
"not_empty": not_empty,
"min_length": min_length(3),
"max_length": max_length(20),
},
messages={
"not_empty": "用户名不能为空",
"min_length": "用户名至少需要3个字符",
"max_length": "用户名不能超过20个字符",
}
)
print(f"有效的用户名: {username}")
except ValidationError as e:
print(f"无效: {e}")
输出:
无效: username: 用户名至少需要3个字符
这种方法扩展性很好。一次性定义你的规则,根据需要组合它们,并获得清晰的错误消息。
安全地导航嵌套字典
访问嵌套字典常常很棘手。当键不存在时会出现 KeyError,当你尝试对字符串进行下标操作时会出现 TypeError,并且你的代码会充斥着链式的 .get() 调用或防御性的 try-except 块。处理来自API的JavaScript对象表示法(JSON)数据时,这更具挑战性。
让我们构建一个安全导航嵌套结构的函数:
from typing import Any, Optional, List, Union
def safe_get(
data: dict,
path: Union[str, List[str]],
default: Any = None,
separator: str = "."
) -> Any:
"""
从嵌套字典中安全获取值。
参数:
data: 要访问的字典
path: 点分隔的路径(例如 "user.address.city")或键列表
default: 路径不存在时返回的值
separator: 分割路径字符串的字符(默认: ".")
返回:
路径处的值,如果未找到则返回 default
"""
# 将字符串路径转换为列表
if isinstance(path, str):
keys = path.split(separator)
else:
keys = path
current = data
for key in keys:
try:
# 处理列表索引(如果是数字则转换字符串为整数)
if isinstance(current, list):
try:
key = int(key)
except (ValueError, TypeError):
return default
current = current[key]
except (KeyError, IndexError, TypeError):
return default
return current
该函数将路径拆分为单个键,并逐步导航嵌套结构。如果任何键不存在,或者你尝试对不可下标操作的对象进行下标操作,它会返回默认值而不是崩溃。
它还能自动处理列表索引。如果当前值是列表且键是数字,它会将键转换为整数。
这是用于设置值的配套函数:
def safe_set(
data: dict,
path: Union[str, List[str]],
value: Any,
separator: str = ".",
create_missing: bool = True
) -> bool:
"""
在嵌套字典中安全设置值。
参数:
data: 要修改的字典
path: 点分隔的路径或键列表
value: 要设置的值
separator: 分割路径字符串的字符
create_missing: 是否创建缺失的中间字典
返回:
成功返回 True,否则返回 False
"""
if isinstance(path, str):
keys = path.split(separator)
else:
keys = path
if not keys:
return False
current = data
# 导航到最后一个键的父级
for key in keys[:-1]:
if key not in current:
if create_missing:
current[key] = {}
else:
return False
current = current[key]
if not isinstance(current, dict):
return False
# 设置最终值
current[keys[-1]] = value
return True
safe_set 函数根据需要创建嵌套结构并设置值。这对于动态构建字典很有用。
测试一下:
# 示例嵌套数据
user_data = {
"user": {
"name": "Anna",
"address": {
"city": "San Francisco",
"zip": "94105"
},
"orders": [
{"id": 1, "total": 99.99},
{"id": 2, "total": 149.50}
]
}
}
# safe_get 示例
city = safe_get(user_data, "user.address.city")
print(f"城市: {city}")
country = safe_get(user_data, "user.address.country", default="Unknown")
print(f"国家: {country}")
first_order = safe_get(user_data, "user.orders.0.total")
print(f"第一笔订单: ${first_order}")
# safe_set 示例
new_data = {}
safe_set(new_data, "user.settings.theme", "dark")
print(f"创建结果: {new_data}")
输出:
城市: San Francisco
国家: Unknown
第一笔订单: $99.99
创建结果: {'user': {'settings': {'theme': 'dark'}}}
这种模式消除了防御性编程的混乱,使你在处理JSON、配置文件或任何深度嵌套的数据时代码更清晰。
对长时间操作强制执行超时
有些操作耗时太长。数据库查询可能挂起,网页抓取操作可能卡在慢速服务器上,或者计算可能永远运行。你需要一种设置时间限制并退出的方法。
这是一个使用线程的超时装饰器:
import threading
import functools
from typing import Callable, Optional
class TimeoutError(Exception):
"""操作超过其超时时间时抛出。"""
pass
def timeout(seconds: int, error_message: Optional[str] = None):
"""
装饰器,用于对函数执行强制执行超时。
参数:
seconds: 最大执行时间(秒)
error_message: 超时的自定义错误消息
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = [TimeoutError(
error_message or f"操作在 {seconds} 秒后超时"
)]
def target():
try:
result[0] = func(*args, **kwargs)
except Exception as e:
result[0] = e
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout=seconds)
if thread.is_alive():
raise TimeoutError(
error_message or f"操作在 {seconds} 秒后超时"
)
if isinstance(result[0], Exception):
raise result[0]
return result[0]
return wrapper
return decorator
这个装饰器在一个单独的线程中运行你的函数,并使用 thread.join(timeout=seconds) 进行等待。如果线程在超时后仍然存活,我们就知道它耗时太长并抛出 TimeoutError。
函数结果存储在一个列表(可变容器)中,以便内部线程可以修改它。如果线程中发生异常,我们在主线程中重新抛出它。
⚠️ 一个限制:即使超时后,线程仍会在后台继续运行。对于大多数用例来说这没问题,但对于有副作用的操作,要小心。
让我们测试一下:
import time
@timeout(2, error_message="查询耗时太长")
def slow_database_query():
"""模拟慢查询。"""
time.sleep(5)
return "查询结果"
@timeout(3)
def fetch_data():
"""模拟快速操作。"""
time.sleep(1)
return {"data": "value"}
# 测试超时
try:
result = slow_database_query()
print(f"结果: {result}")
except TimeoutError as e:
print(f"超时: {e}")
# 测试成功
try:
data = fetch_data()
print(f"成功: {data}")
except TimeoutError as e:
print(f"超时: {e}")
输出:
超时: 查询耗时太长
成功: {'data': 'value'}
这种模式对于构建响应式应用程序至关重要。当你抓取网站、调用外部API或运行用户代码时,超时可以防止程序无限期挂起。
使用自动清理管理资源
打开文件、数据库连接和网络套接字需要仔细清理。如果发生异常,你需要确保资源被释放。使用 with 语句的上下文管理器可以处理这个问题,但有时你需要更多的控制。
让我们构建一个灵活的上下文管理器,用于自动资源清理:
from contextlib import contextmanager
from typing import Callable, Any, Optional
import traceback
@contextmanager
def managed_resource(
acquire: Callable[[], Any],
release: Callable[[Any], None],
on_error: Optional[Callable[[Exception, Any], None]] = None,
suppress_errors: bool = False
):
"""
用于自动资源获取和清理的上下文管理器。
参数:
acquire: 获取资源的函数
release: 释放资源的函数
on_error: 可选的错误处理器
suppress_errors: 是否在清理后抑制异常
"""
resource = None
try:
resource = acquire()
yield resource
except Exception as e:
if on_error and resource is not None:
try:
on_error(e, resource)
except Exception as handler_error:
print(f"错误处理器中的错误: {handler_error}")
if not suppress_errors:
raise
finally:
if resource is not None:
try:
release(resource)
except Exception as cleanup_error:
print(f"清理过程中的错误: {cleanup_error}")
traceback.print_exc()
managed_resource 函数是一个上下文管理器工厂。它接受两个必需函数:一个用于获取资源,一个用于释放资源。release 函数总是在 finally 块中运行,保证即使发生异常也能进行清理。
可选的 on_error 参数让你在异常传播之前处理它们。这对于记录日志、发送警报或尝试恢复很有用。suppress_errors 标志决定异常是被显式抛出还是被抑制。
这是一个演示资源跟踪的辅助类:
class ResourceTracker:
"""辅助类,用于跟踪资源操作。"""
def __init__(self, name: str, verbose: bool = True):
self.name = name
self.verbose = verbose
self.operations = []
def log(self, operation: str):
self.operations.append(operation)
if self.verbose:
print(f"[{self.name}] {operation}")
def acquire(self):
self.log("正在获取资源")
return self
def release(self):
self.log("正在释放资源")
def use(self, action: str):
self.log(f"正在使用资源: {action}")
测试上下文管理器:
# 示例:带错误处理的操作
tracker = ResourceTracker("Database")
def error_handler(exception, resource):
resource.log(f"发生错误: {exception}")
resource.log("正在尝试回滚")
try:
with managed_resource(
acquire=lambda: tracker.acquire(),
release=lambda r: r.release(),
on_error=error_handler
) as db:
db.use("INSERT INTO users")
raise ValueError("重复条目")
except ValueError as e:
print(f"捕获到: {e}")
输出:
[Database] 正在获取资源
[Database] 正在使用资源: INSERT INTO users
[Database] 发生错误: 重复条目
[Database] 正在尝试回滚
[Database] 正在释放资源
捕获到: 重复条目
这种模式对于管理数据库连接、文件句柄、网络套接字、锁以及任何需要保证清理的资源都很有用。它可以防止资源泄漏,使代码更安全。
总结
本文中的每个函数都解决了一个特定的错误处理挑战:重试瞬时故障、系统验证输入、安全访问嵌套数据、防止挂起操作以及管理资源清理。
这些模式在API集成、数据处理管道、网页抓取和面向用户的应用程序中反复出现。
这里的技术使用了装饰器、上下文管理器和可组合函数,使错误处理更少重复、更可靠。你可以按原样将这些函数放入项目中,或者根据具体需求进行调整。它们是自包含的、易于理解的,并能解决你会经常遇到的问题。编码快乐!FINISHED