Hashable & Mutable
Hashable & Mutable 的讨论
基本概念
可变性 (Mutability) 与不可变性 (Immutability)
首先,我们来区分可变对象和不可变对象。
- 可变对象 (Mutable Objects):指的是那些在创建后,其内部状态可以被修改的对象。这意味着你可以在不改变对象在内存中的地址(ID)的情况下,改变其内容。常见可变类型有
list
,dict
,set
,bytearray
等。 - 不可变对象 (Immutable Objects):指的是那些在创建后,其内部状态不能被修改的对象。任何看似修改的操作实际上都是创建了一个新的对象。常见不可变类型有
int
,float
,str
,tuple
,frozenset
,bytes
,NoneType
等。
我们可以使用内置的 id()
函数来获取对象的唯一标识符(通常是其在内存中的地址)。观察 id()
的变化可以帮助我们理解可变性
1 |
|
可哈希性 (Hashability) 与不可哈希性 (unhashability)
可哈希对象 (Hashable Objects):指的是那些满足以下条件的对象:
- 在其生命周期内,其哈希值 (hash value) 是不可变的。
- 可以与其他对象进行相等性比较 (通过
__eq__()
方法) - 如果两个对象相等 (
a == b
为 True),则它们的哈希值必须相等 (hash(a) == hash(b)
)
可哈希性是 Python 中某些数据结构(主要是字典和集合)能否正常工作的基石。内置的 hash()
函数可以用于获取一个对象的哈希值。
1 |
|
为什么需要哈希值?
字典 (Dictionary) 和集合 (Set) 是基于哈希表实现的。当将一个对象作为字典的键或集合的成员时,Python 会首先计算该对象的哈希值,然后使用这个哈希值来快速进行哈希查找。如果两个对象的哈希值相同(哈希碰撞时),Python 还会使用对象的相等性比较 (__eq__()
) 来最终确定它们是否是同一个对象
因此,一个对象的哈希值在其作为字典键或集合成员期间必须保持不变,否则,当你试图查找这个对象时,计算出的新哈希值可能无法指向正确的位置,导致查找失败
即:**可变对象通常是不可哈希的,不可变对象通常是可哈希的。**这个规则在几乎所有的情况下是适用的
基本特性:
哈希值基于“值”还是基于“身份”?
对象的哈希值通常是基于其内容(对于值不可变的类型)或其在内存中的身份(对于默认的用户自定义对象)计算的
1 |
|
原因解析:
- 对于像整数、字符串、以及元素都是可哈希的元组这样的 immutable 的内置类型,它们的
==
运算符进行值比较,它们的hash()
函数计算的哈希值也是基于它们的内容(值)。这意味着只要两个这样的对象内容一样,它们就相等,并且哈希值也一样,即使它们是不同的对象实例(有不同的id()
) - 对于没有自定义
__eq__
和__hash__
的用户自定义对象,默认的==
运算符进行id()
比较,默认的hash()
函数返回一个基于对象id()
的值。这意味着只有当它们是同一个对象实例时才相等,并且哈希值才一样 - Hashable 的核心要求是:如果两个对象相等 (
a == b
is True),那么它们的哈希值必须相等 (hash(a) == hash(b)
)。 反过来不一定成立(不同的对象可能有相同的哈希值,称为哈希碰撞)
Mutable 在局部函数中被改变:
Python 使用的是一种被称为 按对象引用传递(pass-by-object-reference) 的模型。这意味着当你将一个变量传递给函数时,函数接收到的是该变量所指向的对象引用的一个副本。函数内部对参数变量名的任何操作,实际上是操作这个引用副本所指向的对象。
那么,当操作引用所指向的对象时,可变对象和不可变对象就展现出不同的行为:
- 当将一个可变对象(如列表、字典、集合)传递给函数,并在函数内部通过引用修改对象内容时,这种修改会影响到函数外部的原始对象。因为函数内外的引用指向的是同一个对象
- 当将一个不可变对象(如整型、字符串、元组)传递给函数时,如果你在函数内部尝试“修改”它(例如进行数学运算或字符串拼接),这实际上会创建一个新的不可变对象,并将函数内部的局部变量重新指向这个新对象。这不会影响函数外部原始变量所指向的对象
1 |
|
可以看见 mutable 对象在函数内部被改变了,但是 immutable 对象并没有改变
Mutable 概念不存在?
事实上,Mutable 的概念对于程序来说是不存在的,这个概念是人为创造出来来对 object 进行分类的,程序并不会对 object 进行 mutable 和 immutable 进行分类的,我们看一下下面的例子:
1 |
|
上面例子无论是 mutable 对象还是 immutable 对象都没有改变,我们再看一个障眼法:
1 |
|
这里的障眼法,也就是让 mutable 对象和 immutable 对象的表现行为不一样的原因在于,两个 +=
所触发的代码行为是不一样的,对于 i += 1
只是简单做了一个加法,而对 lst
来说是触发了 __iadd__
魔术方法:
我们打印 id 检查一下理论的正确性:
1 |
|
-
元组 (Tuple): 元组本身是不可变的。但是,元组的可哈希性取决于其包含的元素。如果元组中所有元素都是可哈希的(例如,整数、字符串、不可变元组等),那么该元组就是可哈希的。如果元组中包含任何不可哈希的元素(例如,列表、字典、集合),那么该元组就是不可哈希的。
Python
1
2
3
4
5hashable_tuple = (1, "hello", (5, 6))
print(f"Hashable tuple hash: {hash(hashable_tuple)}") # Works
unhashable_tuple = (1, [2, 3])
print(f"Unhashable tuple hash: {hash(unhashable_tuple)}") # TypeError