Python分类器回退实测:改了默认名称,为什么仍会KeyError
一个容易漏掉的回退分支
检查一个版式推荐脚本时,我复现了这样的错误:当文本没有命中任何规则,程序把分类名改为 overview,随后从得分字典读取这个名称,抛出 KeyError。问题已附最小复现提交到项目Issue #11。
下面用自行编写的简化示例说明原因。示例只依赖Python内置功能,六组检查已在Python 3.10.4实际执行通过。
def broken(scores):
key = max(scores, key=scores.get)
if scores[key] == 0:
key = 'overview'
return scores[key]
broken({'process': 0, 'data': 0})
# KeyError: 'overview'
key变量换了值,不会自动向scores增加一项。回退名称在业务上存在,并不代表每个运行时字典都存储了它。调用max之前,如果字典本身为空,还会先遇到另一个问题:没有元素可选。
把分类定义与本次得分分开
分类的展示名称、版式和默认信号属于定义;一次计算得到的分数属于运行结果。这两者可以包含不同的键,但读取时要明确使用哪一个映射。
PATTERNS = {
'process': {'layout': 'flow', 'signals': ['ordered steps']},
'data': {'layout': 'chart', 'signals': ['metrics']},
'overview': {'layout': 'title-body', 'signals': []},
}
def recommend(scores):
key = max(scores, key=scores.get, default='overview')
if scores.get(key, 0) <= 0 or key not in PATTERNS:
key = 'overview'
definition = PATTERNS[key]
return {
'pattern': key,
'layout': definition['layout'],
'signals': list(definition['signals']),
}
这里的max(..., default=...)只解决空映射没有最大项的问题。它不会给映射添加默认键,也不会解决全零分数,因此后面仍然需要判断。分数没有可用的正值,或者获胜的分类不在已知定义里,才选择概述分类,并从定义表读取展示信息。
这个接口约定scores中的值为可比较的数值;示例没有接收字符串分数或None,实际读取外部JSON时应先校验输入。并列最高分如何选择也属于业务规则,本例沿用字典迭代顺序,不把它宣称为唯一正确策略。
实测结果与可变对象边界
assert recommend({'process': 0, 'data': 0})['pattern'] == 'overview'
assert recommend({})['pattern'] == 'overview'
assert recommend({'process': 3, 'data': 1})['pattern'] == 'process'
assert recommend({'unknown': 5})['pattern'] == 'overview'
result = recommend({})
result['signals'].append('temporary')
assert PATTERNS['overview']['signals'] == []
加上最开始捕获KeyError的复现检查,共六组。最后一组验证了返回值中的信号列表可以独立修改:list(...)创建新列表,避免调用者向全局分类定义追加临时内容。这里的列表元素都是字符串,浅复制足够;如果定义中继续嵌套可变对象,需要另行决定隔离深度。
对照原始错误,最小修复可以只在概述分支把信号设为空列表,不一定要重写整个分类器。修复范围取决于现有数据结构:先让全零与空输入稳定返回,再确认流程、数据等已命中分支的行为未被改变。
排查时看完整的输出组装
默认分支的检查不能停在“分类名已经换了”。应继续追踪结果组装时每一个字典索引,确认回退键在哪些映射中存在;也要分别验证空输入、所有分数为零、正常命中和未知分类。这样才能避免默认分支本身成为新的异常入口。
- 点赞
- 收藏
- 关注作者
评论(0)