《流畅的Python》读书笔记11(补充02): 使用一等函数实现设计模式 - 函数策略模式防污染四法
·
核心方案对比:避免全局命名污染的策略
在Python中使用函数替代类实现策略模式时,避免全局命名空间污染是提升代码可维护性的关键。以下是几种主流方案的对比与实践指南。
| 方案 | 核心机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 装饰器注册 | 通过装饰器将策略函数显式注册到模块级集合(列表/字典) | 显式声明、灵活、不依赖命名约定、易于扩展 | 需额外定义装饰器和注册表 | 策略数量多、需动态发现、框架式应用 |
模块内省(globals()) |
扫描模块全局命名空间,按命名约定(如*_promo)自动收集 |
无需手动注册、代码简洁 | 依赖严格命名约定、易误收集、灵活性差 | 小型项目、策略命名高度规范 |
| 闭包+工厂函数 | 在工厂函数内定义策略函数,仅暴露公共接口 | 完全隔离实现、无全局污染 | 创建稍复杂、需理解闭包 | 策略需封装私有状态或数据 |
| 类命名空间封装 | 使用类(或SimpleNamespace)作为策略函数的容器 |
结构化组织、可分组管理 | 仍存在类级命名空间 | 策略自然分组、需关联元数据 |
一、装饰器注册(生产级推荐)
这是最显式、最可控的方式,广泛用于Flask、FastAPI等框架。它通过装饰器将策略函数自动添加到一个模块级注册表中,完全避免在全局命名空间中散落大量策略函数 。
1. 基础实现
# strategy_registry.py
from typing import Callable, Dict, List
from decimal import Decimal
# 策略注册表:使用字典便于按名称检索,或使用列表/集合
_strategy_registry: Dict[str, Callable] = {}
def register_strategy(name: str):
"""装饰器:将策略函数注册到中央仓库"""
def decorator(func: Callable) -> Callable:
if name in _strategy_registry:
raise ValueError(f"策略名称 '{name}' 已存在")
_strategy_registry[name] = func
return func
return decorator
def get_strategy(name: str) -> Callable:
"""获取策略函数"""
return _strategy_registry.get(name)
def list_strategies() -> List[str]:
"""列出所有已注册策略"""
return list(_strategy_registry.keys())
# ===== 策略定义 =====
@register_strategy("fidelity")
def fidelity_promo(order) -> Decimal:
"""积分折扣策略"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
@register_strategy("bulk_item")
def bulk_item_promo(order) -> Decimal:
"""批量折扣策略"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
# ===== 使用示例 =====
def apply_best_promo(order):
"""应用最佳折扣策略"""
best_discount = Decimal(0)
best_strategy = None
for name, strategy in _strategy_registry.items():
discount = strategy(order)
if discount > best_discount:
best_discount = discount
best_strategy = name
if best_strategy:
print(f"应用策略: {best_strategy}, 折扣: {best_discount}")
return best_discount
# 查看所有策略
print("可用策略:", list_strategies()) # 输出: ['fidelity', 'bulk_item']
关键优势 :
- 显式声明:每个策略通过
@register_strategy明确标识,一目了然。 - 无命名约定依赖:不要求函数名遵循特定模式。
- 动态管理:可在运行时添加、删除或替换策略。
- 避免污染:策略函数仅在注册表中被引用,全局命名空间保持整洁。
2. 进阶:自动发现与注册
结合setuptools的入口点(entry points)或标准库的importlib,可实现插拔式策略发现,彻底解耦策略定义与核心系统。
# 通过入口点自动发现(setup.py配置)
# 策略提供方
# setup.py
setup(
entry_points={
'order.strategies': [
'fidelity = my_strategies:fidelity_promo',
'bulk = my_strategies:bulk_item_promo',
],
}
)
# 核心系统动态加载
import pkg_resources
def load_all_strategies():
strategies = {}
for entry_point in pkg_resources.iter_entry_points('order.strategies'):
strategies[entry_point.name] = entry_point.load()
return strategies
二、模块内省(globals())方案
此方案利用Python的反射能力,自动收集符合命名约定的函数。虽然不如装饰器注册显式,但在小型项目或脚本中足够简洁。
# promotions.py
from decimal import Decimal
# 策略函数定义(遵循 _promo 后缀约定)
def fidelity_promo(order):
# ... 实现
def bulk_item_promo(order):
# ... 实现
def _helper_function():
"""辅助函数,不应被收集"""
pass
def best_promo(order):
"""自动发现所有策略"""
# 关键:通过命名约定过滤,避免收集非策略函数
promo_funcs = [
func for name, func in globals().items()
if name.endswith('_promo') and callable(func)
]
return max(promo(order) for promo in promo_funcs)
注意事项 :
- 命名约束:所有策略函数必须遵循相同后缀(如
_promo),否则会被遗漏。 - 潜在污染:若模块中存在其他同名后缀的非策略函数,会被误收集。
- 作用域限制:
globals()仅能访问当前模块的全局命名空间,无法跨模块收集。
三、闭包与工厂函数(完全隔离)
对于需要封装私有状态或数据的策略,可使用工厂函数返回策略函数,实现完全零全局污染。
def create_discount_strategy(discount_rate: Decimal, threshold: int):
"""
工厂函数:创建折扣策略,完全封装在闭包内
参数 discount_rate: 折扣率
参数 threshold: 阈值
返回: 策略函数
"""
def strategy(order) -> Decimal:
# 闭包捕获了 discount_rate 和 threshold
if order.total() > threshold:
return order.total() * discount_rate
return Decimal(0)
return strategy
# 使用工厂创建策略,不污染全局命名空间
premium_strategy = create_discount_strategy(Decimal('0.1'), 1000)
vip_strategy = create_discount_strategy(Decimal('0.15'), 5000)
# 策略可存储在容器中集中管理
strategies = {
'premium': premium_strategy,
'vip': vip_strategy
}
# 应用策略
order = Order(...)
best_discount = max(strategy(order) for strategy in strategies.values())
闭包优势 :
- 状态封装:策略的配置参数(折扣率、阈值)被闭包捕获,无需全局变量。
- 零污染:策略函数仅在工厂函数作用域内创建,全局命名空间无痕迹。
- 动态生成:可根据运行时参数生成不同策略。
四、类命名空间封装(结构化组织)
当策略需要分组或关联元数据时,可使用类作为命名空间容器。
class DiscountStrategies:
"""策略命名空间容器"""
@staticmethod
def fidelity(order):
# ... 实现
@staticmethod
def bulk_item(order):
# ... 实现
# 类属性存储策略元数据
DESCRIPTIONS = {
'fidelity': '积分折扣策略',
'bulk_item': '批量购买折扣'
}
# 使用
strategies = [DiscountStrategies.fidelity, DiscountStrategies.bulk_item]
best_discount = max(s(order) for s in strategies)
# 通过类访问描述
print(DiscountStrategies.DESCRIPTIONS['fidelity'])
适用场景:
- 策略逻辑相似,可分组管理。
- 需要为策略附加元数据(描述、版本等)。
- 项目已采用面向对象架构,需保持风格一致。
五、综合对比与选型建议
1. 方案选型决策树
是否需要完全避免全局符号?
├── 是 → 使用【闭包+工厂函数】方案
└── 否 → 策略是否需要动态发现/注册?
├── 是 → 使用【装饰器注册】方案
└── 否 → 策略数量少且命名规范?
├── 是 → 使用【模块内省】方案
└── 否 → 使用【类命名空间】方案
2. 生产环境最佳实践
结合装饰器注册与配置化,实现可插拔的策略管理系统:
# config.py
STRATEGY_CONFIG = {
'fidelity': {
'module': 'strategies.promo',
'function': 'fidelity_promo',
'enabled': True,
'priority': 1
},
'bulk_item': {
'module': 'strategies.promo',
'function': 'bulk_item_promo',
'enabled': True,
'priority': 2
}
}
# strategy_manager.py
import importlib
from typing import Dict, Any
class StrategyManager:
"""策略管理器:按配置动态加载策略"""
def __init__(self, config: Dict[str, Any]):
self._strategies = {}
self._load_strategies(config)
def _load_strategies(self, config):
for name, cfg in config.items():
if not cfg.get('enabled', True):
continue
module = importlib.import_module(cfg['module'])
func = getattr(module, cfg['function'])
self._strategies[name] = {
'func': func,
'priority': cfg.get('priority', 99)
}
def get_strategy(self, name):
return self._strategies.get(name)
def list_enabled(self):
return sorted(
self._strategies.items(),
key=lambda x: x[1]['priority']
)
# 初始化管理器
manager = StrategyManager(STRATEGY_CONFIG)
# 使用策略
for name, info in manager.list_enabled():
strategy_func = info['func']
discount = strategy_func(order)
# ... 处理折扣
3. 关键注意事项
- 避免可变默认参数:策略函数若使用可变默认参数(如
def func(items=[])),可能导致状态污染。应使用None作为默认值,在函数内初始化 。 - 性能考量:装饰器注册在模块导入时一次性完成,无运行时开销。
globals()内省每次调用都需遍历全局字典,在频繁调用场景可能影响性能。 - 测试友好性:装饰器注册和工厂函数方案更易于模拟(mock)和替换策略,便于单元测试。
通过上述方案,可有效管理策略函数,避免全局命名空间污染,同时保持代码的灵活性与可维护性。装饰器注册方案因其显式性和可扩展性,成为多数生产项目的首选。
参考来源
更多推荐


所有评论(0)