Python Enum作为可扩展TRPG骰子roll标记的用法及性能疑问
DnD骰子计算器:暴击率缓存与兼容方案优化问题
项目背景
我重启了一个用于DnD等TRPG的骰子计算器个人旧项目,计划用Python Enum预定义d20的常见掷骰分布(如普通掷骰、优势、劣势、精灵精准等),代码示例如下:
class d20(Enum): NORMAL = d(20) ADVANTAGE = d(20).advantage() DISADVANTAGE = d(20).disadvantage() ELVEN_ACCURACY = intdist.max(d(20).advantage(), d(20))
我将该Enum用于关联的AttackRoll类,伪代码如下:
class AttackRoll: def __init__(self, attack_modifier, crit_range = 20): self.crit_chances = [] # 待确定的容器,和我的问题相关 for distribution in d20: self.crit_chances.add(distribution, crit_chance(distribution, crit_range)) def get_crit_chance(self, d20_enum): return self.crit_chances[d20_enum]
核心需求
我的核心需求是为Enum中定义的每种d20掷骰类型关联暴击率,同时希望get_crit_chance方法既快速又符合Python风格:
- 部分衍生属性计算开销较高,需要缓存常用值;
- 不想局限于预定义的Enum类型,希望支持鸭子类型,兼容自定义d20类分布。
现有方案
我想到的方案是用字典缓存Enum对应的暴击率,通过try/except处理非Enum类型的兼容逻辑,伪代码如下:
class AttackRoll: def __init__(self, attack_modifier, crit_range = 20): self.crit_chances = dict() for distribution in d20: self.crit_chances[distribution] = crit_chance(distribution, crit_range) def get_crit_chance(self, d20_enum): try: return self.crit_chances[d20_enum] except KeyError: if is_d20_roll_like(d20_enum): return crit_chance(d20_enum, crit_range) raise ValueError("不支持的掷骰类型")
问题
- 是否有更优的实现方案?
- 我担心过早优化:复杂缓存操作最多增加200微秒开销,而目标函数仅10微秒,实际调用数千次,是否需要关注该优化?
解答
一、更优方案建议
1. 结合预缓存字典与lru_cache实现双重缓存
这种方案兼顾Enum类型的极速访问和自定义类型的自动缓存,代码更简洁:
from functools import lru_cache class AttackRoll: def __init__(self, attack_modifier, crit_range=20): self.crit_range = crit_range # 预缓存所有Enum类型的暴击率,直接走字典查找 self.crit_chances = {dist: self._calc_crit(dist) for dist in d20} @lru_cache(maxsize=None) def _calc_crit(self, distribution): # 统一的暴击率计算逻辑,自动缓存自定义类型的结果 return crit_chance(distribution, self.crit_range) def get_crit_chance(self, d20_roll): # 优先查预缓存的Enum值,速度最快 if d20_roll in self.crit_chances: return self.crit_chances[d20_roll] # 鸭子类型判断:检查必要属性/方法,比如是否有概率计算接口 if hasattr(d20_roll, 'probability') or hasattr(d20_roll, 'roll'): return self._calc_crit(d20_roll) raise ValueError("不支持的掷骰类型")
优势:
- Enum类型直接走字典O(1)查找,无额外开销;
- 自定义类型自动利用
lru_cache缓存计算结果,无需手动维护缓存字典; - 代码结构清晰,符合Python的简洁风格。
2. 可选:实现__getitem__简化调用
如果想让调用更直观,可以给AttackRoll类添加__getitem__方法:
def __getitem__(self, d20_roll): return self.get_crit_chance(d20_roll)
之后就可以像字典一样直接访问:attack_roll[d20.NORMAL]
二、关于过早优化的判断
从数据来看:
- 按5000次调用计算,总额外开销为
5000 * 200μs = 1秒,这个量级在桌面应用中完全感知不到,除非你的计算器是高频调用的服务端程序。
结论:不需要刻意关注这个优化。当前方案的性能已经足够满足需求,与其花时间优化微秒级的开销,不如把精力放在逻辑正确性、代码可维护性上——比如完善鸭子类型的判断规则、处理自定义分布的边界情况等。
内容的提问来源于stack exchange,提问作者Minty
相关产品推荐
相关产品推荐

