You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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("不支持的掷骰类型")

问题

  1. 是否有更优的实现方案?
  2. 我担心过早优化:复杂缓存操作最多增加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:07:34