Python中列表不可哈希,含列表属性的类实例为何可哈希?
问题示例
首先定义一个普通列表:
ingredients = ["hot water", "taste"]
尝试打印该列表的哈希值会触发TypeError:
print(hash(ingredients)) >>> TypeError: unhashable type: 'list'
这意味着列表无法用作字典键等需要可哈希类型的场景。
但如果定义一个仅接收列表作为参数的Tea类:
class Tea: def __init__(self, ingredients: list|None = None) -> None: self.ingredients = ingredients if ingredients is None: self.ingredients = []
创建该类实例并打印其哈希值不会触发错误:
cup = Tea(["hot water", "taste"]) print(hash(cup)) >>> 269041261
这表明该实例是可哈希的,但打印其ingredients属性的哈希值仍会触发预期错误:
print(hash(cup.ingredients)) >>> TypeError: unhashable type: 'list'
甚至可以将该实例用作字典键:
dct = { cup: "test" }
核心疑问:为什么包含不可哈希列表的类实例本身是可哈希的?衍生问题:是否可以通过这种方式将列表(或其他不可哈希类型)用作字典键?
核心问题解答
Python中对象的可哈希性由**__hash__()方法决定。默认情况下,用户自定义类的实例会继承object类的__hash__()实现——该实现基于对象的内存地址**计算哈希值,完全不涉及实例内部的属性内容。
也就是说,只要你没有重写类的__hash__()或__eq__()方法,不管实例内部存储了什么不可哈希的属性,实例本身的哈希值只和它在内存中的位置有关,因此不会触发哈希错误。
列表不可哈希的原因是它属于可变类型,Python没有为其实现合法的__hash__()方法(可变对象的哈希值若随内容变化,会破坏字典、集合等哈希表结构的一致性)。但这和包含它的类实例无关——类实例的哈希计算逻辑不涉及内部属性,自然不受影响。
衍生问题解答
这种方式确实能让“包含列表的对象”作为字典键,但存在明显的逻辑缺陷:
- 两个内容完全相同的
Tea实例会被视为不同的键,因为它们的内存地址不同:cup1 = Tea(["hot water", "taste"]) cup2 = Tea(["hot water", "taste"]) dct = {cup1: "test1", cup2: "test2"} print(dct) # 会同时存在两个独立的键值对 - 如果后续修改实例的
ingredients属性,实例的哈希值不会改变(因为基于内存地址),这会导致逻辑不一致——比如修改cup1.ingredients后,它在字典中的键仍对应原来的哈希值,和实际内容脱节。
如果想让包含列表的对象成为有意义的字典键,需要手动重写__hash__()和__eq__()方法:
- 将列表转换为不可变类型(如
tuple)来计算哈希值 - 让
__eq__()方法基于属性内容判断对象是否相等
示例实现:
class Tea: def __init__(self, ingredients: list|None = None) -> None: # 将输入的列表转为元组,确保属性不可变 self.ingredients = tuple(ingredients) if ingredients else tuple() def __hash__(self): # 基于不可变的元组计算哈希值 return hash(self.ingredients) def __eq__(self, other): if not isinstance(other, Tea): return False return self.ingredients == other.ingredients
这样,内容相同的实例会被视为同一个键,同时因为ingredients被设为不可变元组,避免了后续修改导致的哈希不一致问题。
内容的提问来源于stack exchange,提问作者juanpethes

