Python 字典的相等键:True、1 和 1.0,为什么最终只剩一个位置
汇总几类配置时,用 True 表示开关,用整数一表示编号,用浮点一表示测量值,觉得三种类型应该占三个字典位置。实际逐个写入后长度却只有一,最后的文字覆盖了前两份。原因是普通字典采用键的相等语义,这几个内置数值对象彼此相等,可以索引同一条目。
这不是字符串转换,也不是字典把类型偷偷改成统一类型。原输入仍有不同类型,只是类型差异没有进入键的区分条件。若业务必须区分“开关真”和“编号一”,应该在设计键时表达这层区别,不能仅凭打印出来的外观推断条目会独立保存。
可直接运行的对照实验
保存为 demo.py 后用 Python 3.12 运行。第一段故意覆盖同一条目,随后使用带类型标签的元组保留三种含义。最后写一个只接受内置整数的入口,同时放入布尔值与浮点值作反例,明确接口选择了多严格的类型政策。
AI概念插图:外形不同但相等的数值键汇入同一条目,需要额外身份信息才能分开。图片不是运行截图,也不表示实测性能。
assert True == 1 == 1.0
assert hash(True) == hash(1) == hash(1.0)
values = {}
for key, label in [(True, "bool"), (1, "int"), (1.0, "float")]:
values[key] = label
assert len(values) == 1
assert values[True] == values[1] == values[1.0] == "float"
print("plain dictionary:", len(values), values[1])
tagged = {(type(key), key): label
for key, label in [(True, "bool"), (1, "int"), (1.0, "float")]}
assert len(tagged) == 3
assert tagged[(bool, True)] == "bool"
assert tagged[(int, 1)] == "int"
print("type-tagged dictionary:", len(tagged))
class Collision:
def __init__(self, name):
self.name = name
def __hash__(self):
return 7
def __eq__(self, other):
return isinstance(other, Collision) and self.name == other.name
assert len({Collision("a"): 1, Collision("b"): 2}) == 2
print("same hash, unequal keys: 2 entries")
def require_id(value):
if type(value) is not int:
raise TypeError("built-in integer required")
return value
assert require_id(1) == 1
assert isinstance(True, int)
for value in [True, 1.0]:
try:
require_id(value)
except TypeError:
pass
else:
raise AssertionError("wrong type accepted")
print("boolean and float IDs rejected")哈希用于查找,相等才决定是否同键
程序先证明 True == 1 == 1.0,而且它们的哈希一致;字典长度为一,通过任意一种键都读到最后写入的 float。相等的内置数值在这里必须表现出一致的哈希行为,字典才能把等价索引映射到同一位置。
反过来,哈希值相同并不保证两个键相等。程序定义两个故意返回同一哈希的对象,但按 name 比较;放入字典后仍保留两项。这是小型碰撞实验,用来排除“同一个哈希必定覆盖”的错误解释,不能用两项数据推断实际哈希表性能。
选择合并语义,或者保留类型身份
如果三种输入在业务上确实都代表数量一,合并可能正是需要的结果。此时应在入口明确允许的值域与转换规则,再统一生成键。若三者属于不同领域,则像示例那样以 type(value) 与 value 组成元组,可以在当前内存索引中保留类型身份。
类型对象组成的键不适合直接当作跨语言传输格式。持久化或交换数据时,可改用经过约定的字符串领域标签和原始值,例如开关、编号、测量分别使用不同标签。标签必须由应用控制,不能让调用方随意拼写后就绕过验证。
整数检查也要考虑布尔值
isinstance(True, int) 为真,因为 bool 是 int 的子类。如果接口要求的编号只能是内置整数,type(value) is int 可以明确拒绝布尔和浮点输入;本例采用这个窄约定,也因此会拒绝自定义整数子类。这是业务选择,不应包装成所有数值接口的通用标准。
如果需要接受更多整数协议类型,就要为它们单独规定转换,再明确处理布尔值,不能先调用 int 把差异抹掉再检查。编号一与字符串一是否等价、浮点一是否允许,也应由输入契约决定。数据库键、缓存键与校验代码最好共享同一份约定。
验收时除了核对最后的值,还要核对条目数量、不同输入是否能够独立取回,以及拒绝分支有没有执行。字典覆盖通常不会报错,只有在写入后检查业务身份数量,才容易发现某类数据已经被另一类吞掉。
资料核对日期:2026年10月2日(北京时间)。代码在 CPython 3.12.14 中独立运行。


