【Bug已解决】special_tokens, ‘list‘ object has no attribute ‘keys‘ 解决方案
2026/8/4 17:13:01 网站建设 项目流程

【Bug已解决】special_tokens, 'list' object has no attribute 'keys' 解决方案

一、现象长什么样

在配置或加载 tokenizer 的special_tokens时,代码突然抛:

AttributeError: 'list' object has no attribute 'keys'

而且报错往往发生在 transformers 内部,不是你自己的代码,stack 类似:

File ".../tokenization_utils_base.py", in _add_tokens for k in special_tokens.keys(): AttributeError: 'list' object has no attribute 'keys'

最迷惑的是:你明明"按文档"传了 special tokens,为什么它要调.keys()?原因是某些入口期望special_tokens是一个{token_type: token_string}的字典(如{"bos_token": "<bos>"}),而你传了一个 list(如["<bos>", "<eos>"])。当代码对这个值调.keys()时,list 没有这个方法,于是炸。

这个 bug 的本质是类型契约不一致:transformers 里add_special_tokens接受 dict,但add_tokens(加普通词表 token)接受 list;很多人把两者搞混,对 special tokens 也传了 list,触发.keys()报错。

二、背景

transformers 的 tokenizer 有两个相关但签名不同的接口:

  1. add_tokens(list):往词表追加普通 token,参数是 list of str。
  2. add_special_tokens(dict):设置特殊 token,参数是{角色: token}的字典,例如{"bos_token": "<bos>", "eos_token": "<eos>"}

内部实现里,add_special_tokens拿到参数后会遍历special_tokens.keys()来逐个角色设置。如果你错误地传了一个 list(比如从某个配置里读出来的[...]),for k in special_tokens.keys()就变成for k in [..].keys()AttributeError: 'list' object has no attribute 'keys'

常见触发场景:

  • 从 JSON/YAML 配置读取 special tokens,格式写成了数组而非对象;
  • add_tokens的习惯套到 special tokens 上;
  • 某些版本文档示例用了 list,但代码实际要 dict(版本漂移);
  • 自己封装的build_special_tokens函数返回了 list,却被传给期望 dict 的入口。

下面用可运行代码复现"期望 dict 收到 list 调.keys()报错"的机制。

三、根因

根因一句话:special_tokens的消费者(如add_special_tokens)期望一个 dict 并调用.keys(),但调用方传入了 list,list 没有.keys()方法,于是AttributeError: 'list' object has no attribute 'keys'

三个具体失配:

  1. 类型契约混淆add_special_tokens要 dict,误传 list(与add_tokens的 list 签名混淆)。
  2. 配置格式错误:special tokens 在配置文件里写成数组而非对象。
  3. 版本漂移:文档/旧代码用 list,新代码要 dict。

四、最小可运行复现

用纯 Python 模拟"期望 dict 的函数收到 list 调.keys()报错":

from dataclasses import dataclass from typing import Union, List, Dict def add_special_tokens(special_tokens: Dict[str, str]): """模拟 transformers 内部:遍历 special_tokens.keys()。""" for role in special_tokens.keys(): print(f"设置 {role} -> {special_tokens[role]}") def main(): # 错误:传了 list 而非 dict bad = ["<bos>", "<eos>", "<pad>"] try: add_special_tokens(bad) except AttributeError as e: print("复现到报错:", e) # 正确:传 dict good = {"bos_token": "<bos>", "eos_token": "<eos>", "pad_token": "<pad>"} add_special_tokens(good) if __name__ == "__main__": main()

运行会先打印复现到报错: 'list' object has no attribute 'keys',再打印正确路径——正是 special tokens 类型错配的本质。

五、解决方案(第一层:最小直接修复)

最立竿见影的修复:统一 special tokens 的表示,永远用 dict({角色: token})传给add_special_tokens;若你手头只有 list,先把它映射成 dict。

def list_to_special_dict(tokens: list, roles=None): """修复:把 list 形式的 special tokens 转成 {角色: token} 字典。""" if roles is None: # 约定顺序:bos, eos, pad, unk ... 按需调整 roles = ["bos_token", "eos_token", "pad_token", "unk_token"] if len(tokens) > len(roles): raise ValueError("token 数量超过预设角色数量") return {roles[i]: t for i, t in enumerate(tokens)} def main(): raw = ["<bos>", "<eos>", "<pad>"] spec = list_to_special_dict(raw) print("转为 dict:", spec) # 现在可安全传给 add_special_tokens(spec) if __name__ == "__main__": main()

第一层修复让 special tokens 在传给add_special_tokens前一定是 dict,.keys()调用不再报错。

六、解决方案(第二层:结构性改进)

把"special tokens 必须 dict"收口成一个SpecialTokens规范类型,在传给任何 transformers 入口前做类型归一化与校验,list 自动转 dict,非法类型直接报错。

from dataclasses import dataclass, field from typing import Dict, List, Union @dataclass class SpecialTokens: data: Dict[str, str] = field(default_factory=dict) @classmethod def coerce(cls, value: Union[Dict, List, "SpecialTokens"]): if isinstance(value, SpecialTokens): return value if isinstance(value, dict): return cls(data=value) if isinstance(value, list): # 自动映射为默认角色 roles = ["bos_token", "eos_token", "pad_token", "unk_token", "mask_token"] if len(value) > len(roles): raise ValueError("list 过长,无法映射到默认角色") return cls(data={roles[i]: v for i, v in enumerate(value)}) raise TypeError(f"special_tokens 必须是 dict 或 list,收到 {type(value)}") def as_dict(self) -> Dict[str, str]: return self.data def main(): # 无论上游给 list 还是 dict,都归一化为 dict for raw in (["<bos>", "<eos>"], {"eos_token": "<eos>"}): spec = SpecialTokens.coerce(raw) print("归一化结果:", spec.as_dict()) if __name__ == "__main__": main()

第二层的关键是SpecialTokens.coerce:调用方不必关心上游给的是 list 还是 dict,统一收口成 dict,且对非法类型显式报错,彻底消除.keys()的 AttributeError。

七、解决方案(第三层:断言 / CI 守护)

加 pytest 守护:(1) dict 输入原样保留;(2) list 输入被自动转 dict;(3) 非 list/dict 类型必须被拒;(4) 转后的 dict 可被.keys()安全调用。

import pytest class SpecialTokens: def __init__(self, data): self.data = data @classmethod def coerce(cls, v): if isinstance(v, dict): return cls(v) if isinstance(v, list): roles = ["bos_token", "eos_token", "pad_token"] return cls({roles[i]: x for i, x in enumerate(v)}) raise TypeError("must be dict or list") def test_dict_passthrough(): s = SpecialTokens.coerce({"bos_token": "<b>"}) assert "bos_token" in s.data.keys() def test_list_coerced(): s = SpecialTokens.coerce(["<b>", "<e>"]) assert s.data == {"bos_token": "<b>", "eos_token": "<e>"} def test_invalid_type_rejected(): with pytest.raises(TypeError): SpecialTokens.coerce(123) def test_keys_callable(): s = SpecialTokens.coerce(["<b>"]) assert callable(getattr(s.data, "keys", None)) if __name__ == "__main__": pytest.main([__file__, "-q"])

CI 里test_list_coerced+test_keys_callable通过,就能保证 special tokens 永远是 dict、.keys()永远可用,从根上防住'list' object has no attribute 'keys'回归。

八、排查清单

遇到'list' object has no attribute 'keys'且与 special_tokens 相关时,按此顺序查:

  1. 定位报错里的.keys()调用:基本锁定某个把 special_tokens 当 dict 处理的入口。
  2. 检查你传的是 list 还是 dictadd_special_tokens要 dict;add_tokens才要 list——别混。
  3. 检查配置文件格式:special tokens 在 JSON/YAML 里应是对象{}而非数组[]
  4. 确认 transformers 版本:老文档可能用 list,新代码要 dict,按当前版本签名为准。
  5. 用 SpecialTokens.coerce 兜底:入口处统一归一化,list 自动转 dict。
  6. 打印实际类型print(type(special_tokens))确认传入的是 dict。
  7. 不要手动拼 special_tokens_map:让add_special_tokens(dict)去填tokenizer.special_tokens_map,别自己构造可能错类型的结构。

九、小结

special_tokens, 'list' object has no attribute 'keys'这个报错,根因不在 transformers 内部有 bug,而在类型契约不一致:special tokens 的消费者(如add_special_tokens)期望 dict 并调用.keys(),但调用方误传了 list(把add_tokens(list)的习惯套到了 special tokens 上,或配置文件把 special tokens 写成了数组)。list 没有.keys(),于是 AttributeError。

修复三层:第一层,永远用{角色: token}的 dict 传给add_special_tokens,list 先映射成 dict;第二层用SpecialTokens.coerce在入口统一归一化,list 自动转 dict、非法类型显式报错;第三层用 pytest 断言"dict 原样保留、list 自动转 dict、.keys()永远可用"。记住:special tokens 是 dict,普通 tokens 是 list——add_special_tokensadd_tokens的签名别搞混。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询