
本文深入剖析 Python 字典出现 KeyError 的典型陷阱:即使键的哈希值看似存在,仍因对象身份(identity)与相等性(equality)逻辑不一致导致查找失败,重点聚焦 __hash__ 与 __eq__ 协议的正确实现。
本文深入剖析 python 字典出现 keyerror 的典型陷阱:即使键的哈希值看似存在,仍因对象身份(identity)与相等性(equality)逻辑不一致导致查找失败,重点聚焦 `__hash__` 与 `__eq__` 协议的正确实现。
在你提供的 Wave Function Collapse 实现中,KeyError 并非源于字典未存储对应键,而是源于 字典键查找机制的本质缺陷:Python 字典依赖 __hash__() 定位桶(bucket),再用 __eq__() 精确比对键对象。若二者逻辑不一致,就会出现“哈希命中但查找失败”的经典问题。
观察你的 Tile 类:
def __eq__(self, value: object) -> bool:
if type(value) != Tile:
return False
return self.img == value.img # ⚠️ 危险!pygame.Surface 不可哈希且==行为不稳定
def __hash__(self) -> int:
return self.imgPath.__hash__() # ✅ 仅基于路径哈希问题核心在于:
✅ __hash__ 基于 imgPath(稳定、可预测);
❌ __eq__ 却基于 self.img == value.img —— 而 pygame.Surface 的 == 操作 未被重载,默认比较对象身份(is),且 Surface 对象每次 pygame.image.load() 都生成新实例,即使路径相同,img 属性也永不相等。
因此:
- TileFactory.createBlank() 第一次调用 → 创建新 Tile('./tiles/blank.png'),存入字典;
- 后续 TileFactory.createBlank() 再次调用 → 创建另一个全新 Tile 实例(路径相同,但 img 是不同对象);
- 新实例 __hash__() 与原实例相同(因 imgPath 相同),字典会去同一桶查找;
- 但 __eq__() 比较时 new_tile.img == old_tile.img 返回 False(两个不同 Surface 对象),字典判定“键不存在”,抛出 KeyError。
✅ 正确修复方案
必须保证 __eq__ 和 __hash__ 严格基于同一组不可变属性。由于 imgPath 是唯一稳定标识,应统一以此为准:
立即学习“Python免费学习笔记(深入)”;
class Tile:
def __init__(self, imgPath: str) -> None:
self.imgPath = imgPath # ✅ 关键:只保留路径作为唯一标识
self.img: pygame.Surface = pygame.image.load(imgPath)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Tile): # 推荐用 isinstance 而非 type()
return False
return self.imgPath == other.imgPath # ✅ 与 __hash__ 保持一致
def __hash__(self) -> int:
return hash(self.imgPath) # ✅ 使用内置 hash(),更安全
def __str__(self) -> str:
return f"Tile({self.imgPath})"
def __repr__(self) -> str:
return self.__str__()? 验证与调试技巧
避免直接用 dict[key] 触发异常,改用安全访问模式:
# 方式1:先检查存在性(推荐用于调试)
blank_tile = TileFactory.createBlank()
if blank_tile in ruleSets:
print("Found:", ruleSets[blank_tile])
else:
print("NOT FOUND — likely hash/equality mismatch")
# 方式2:使用 get() 提供默认值
rule_set = ruleSets.get(blank_tile, TileRuleSet()) # 返回空规则集而非报错⚠️ 重要注意事项
-
Flyweight 模式必须启用:你的 TileFactory._createdTiles 已设计为缓存,但当前 createBlank() 等方法未将新建 Tile 写回缓存,导致每次调用都创建新对象。务必在首次创建后存入字典:
@staticmethod def createBlank() -> Tile: if TileFactory.BLANK_ID not in TileFactory._createdTiles: tile = Tile('./tiles/blank.png') TileFactory._createdTiles[TileFactory.BLANK_ID] = tile return TileFactory._createdTiles[TileFactory.BLANK_ID] - 禁止在 __hash__ 中使用可变属性:imgPath 是字符串(不可变),安全;若误用 self.img(可变且不可哈希),将直接引发 TypeError。
- Pygame Surface 不适合做字典键:其内存地址和内部状态不稳定,永远不要将其用于 == 或 hash。
✅ 总结
KeyError 的根源从来不是“字典没存”,而是“存了却找不到”。解决此类问题的黄金法则:
__eq__ 和 __hash__ 必须基于完全相同的、不可变的属性定义,且该属性能唯一标识对象身份。
修复 Tile 的相等性逻辑并启用 Flyweight 缓存后,createAll() 将稳定运行,波函数坍缩算法的规则映射也将真正可靠。


















