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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 可变对象示例:列表
my_list = [1, 2, 3]
print(f"Original list ID: {id(my_list)}")
my_list.append(4)
print(f"After append list ID: {id(my_list)}") # ID 保持不变
print(f"List content: {my_list}")

# 不可变对象示例:字符串
my_string = "hello"
print(f"Original string ID: {id(my_string)}")
my_string = my_string + " world" # 实际上创建了一个新字符串
print(f"After concatenation string ID: {id(my_string)}") # ID 改变
print(f"String content: {my_string}")

# 不可变对象示例:元组
my_tuple = (1, 2, 3)
print(f"Original tuple ID: {id(my_tuple)}")
# my_tuple[0] = 99 # 尝试修改元组元素会引发 TypeError
new_tuple = my_tuple + (4,) # 实际上创建了一个新元组
print(f"After concatenation tuple ID: {id(new_tuple)}") # ID 改变
print(f"Tuple content: {new_tuple}")

可哈希性 (Hashability) 与不可哈希性 (unhashability)

可哈希对象 (Hashable Objects):指的是那些满足以下条件的对象:

  1. 在其生命周期内,其哈希值 (hash value) 是不可变的。
  2. 可以与其他对象进行相等性比较 (通过 __eq__() 方法)
  3. 如果两个对象相等 (a == b 为 True),则它们的哈希值必须相等 (hash(a) == hash(b))

可哈希性是 Python 中某些数据结构(主要是字典和集合)能否正常工作的基石。内置的 hash() 函数可以用于获取一个对象的哈希值。

1
2
3
4
5
print(f"Hash of integer 10: {hash(10)}")
print(f"Hash of string 'hello': {hash('hello')}")
print(f"Hash of tuple (1, 2): {hash((1, 2))}")

# print(f"Hash of list [1, 2]: {hash([1, 2])}") # 会引发 TypeError

为什么需要哈希值?

​ 字典 (Dictionary) 和集合 (Set) 是基于哈希表实现的。当将一个对象作为字典的键或集合的成员时,Python 会首先计算该对象的哈希值,然后使用这个哈希值来快速进行哈希查找。如果两个对象的哈希值相同(哈希碰撞时),Python 还会使用对象的相等性比较 (__eq__()) 来最终确定它们是否是同一个对象

​ 因此,一个对象的哈希值在其作为字典键或集合成员期间必须保持不变,否则,当你试图查找这个对象时,计算出的新哈希值可能无法指向正确的位置,导致查找失败

​ 即:**可变对象通常是不可哈希的,不可变对象通常是可哈希的。**这个规则在几乎所有的情况下是适用的

基本特性:

哈希值基于“值”还是基于“身份”?

​ 对象的哈希值通常是基于其内容(对于值不可变的类型)或其在内存中的身份(对于默认的用户自定义对象)计算的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
s1 = "hello"
s2 = "hello"
print(f"s1 == s2: {s1 == s2}") # True (基于值比较)
print(f"ID of s1: {id(s1)}")
print(f"ID of s2: {id(s2)}") # 可能相同(字符串驻留)或不同
print(f"Hash of s1: {hash(s1)}")
print(f"Hash of s2: {hash(s2)}") # 哈希值相同 (基于值)

t1 = (1, 2)
t2 = (1, 2)
print(f"t1 == t2: {t1 == t2}") # True (基于值比较)
print(f"ID of t1: {id(t1)}")
print(f"ID of t2: {id(t2)}") # 通常不同
print(f"Hash of t1: {hash(t1)}")
print(f"Hash of t2: {hash(t2)}") # 哈希值相同 (基于值)

o1 = object() # 默认对象,没有定义 __eq__ 和 __hash__
o2 = object()
print(f"o1 == o2: {o1 == o2}") # False (基于身份比较)
print(f"ID of o1: {id(o1)}")
print(f"ID of o2: {id(o2)}") # 不同
print(f"Hash of o1: {hash(o1)}") # 哈希值基于 ID
print(f"Hash of o2: {hash(o2)}") # 哈希值不同 (基于 ID)

# 关键规则:如果 obj1 == obj2 为 True,则 hash(obj1) 必须 == hash(obj2)
# 反之不成立 (哈希碰撞)

原因解析:

  • 对于像整数、字符串、以及元素都是可哈希的元组这样的 immutable 的内置类型,它们的 == 运算符进行值比较,它们的 hash() 函数计算的哈希值也是基于它们的内容(值)。这意味着只要两个这样的对象内容一样,它们就相等,并且哈希值也一样,即使它们是不同的对象实例(有不同的 id()
  • 对于没有自定义 __eq____hash__ 的用户自定义对象,默认的 == 运算符进行 id() 比较,默认的 hash() 函数返回一个基于对象 id() 的值。这意味着只有当它们是同一个对象实例时才相等,并且哈希值才一样
  • Hashable 的核心要求是:如果两个对象相等 (a == b is True),那么它们的哈希值必须相等 (hash(a) == hash(b) )。 反过来不一定成立(不同的对象可能有相同的哈希值,称为哈希碰撞)

Mutable 在局部函数中被改变:

​ Python 使用的是一种被称为 按对象引用传递(pass-by-object-reference) 的模型。这意味着当你将一个变量传递给函数时,函数接收到的是该变量所指向的对象引用的一个副本。函数内部对参数变量名的任何操作,实际上是操作这个引用副本所指向的对象。

那么,当操作引用所指向的对象时,可变对象和不可变对象就展现出不同的行为:

  • 当将一个可变对象(如列表、字典、集合)传递给函数,并在函数内部通过引用修改对象内容时,这种修改会影响到函数外部的原始对象。因为函数内外的引用指向的是同一个对象
  • 当将一个不可变对象(如整型、字符串、元组)传递给函数时,如果你在函数内部尝试“修改”它(例如进行数学运算或字符串拼接),这实际上会创建一个新的不可变对象,并将函数内部的局部变量重新指向这个新对象。这不会影响函数外部原始变量所指向的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
l=[]# mutable?
a= 2 # immutable?

def f(lst):
lst.append(1)

def g(i):
i = 1

f(1)
g(a)

print(1)
print(a)

#>>>>> output: >>>>>
[1]
2

可以看见 mutable 对象在函数内部被改变了,但是 immutable 对象并没有改变

Mutable 概念不存在?

​ 事实上,Mutable 的概念对于程序来说是不存在的,这个概念是人为创造出来来对 object 进行分类的,程序并不会对 object 进行 mutable 和 immutable 进行分类的,我们看一下下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
l=[]# mutable?
a= 2 # immutable?

def f(lst):
lst = [1]

def g(i):
i = 1

f(1)
g(a)

print(1)
print(a)

#>>>>> output: >>>>>
[]
2

上面例子无论是 mutable 对象还是 immutable 对象都没有改变,我们再看一个障眼法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
l=[]# mutable?
a= 2 # immutable?

def f(lst):
lst += [1]

def g(i):
i += 1

f(1)
g(a)

print(1)
print(a)

#>>>>> output: >>>>>
[1]
2

​ 这里的障眼法,也就是让 mutable 对象和 immutable 对象的表现行为不一样的原因在于,两个 += 所触发的代码行为是不一样的,对于 i += 1 只是简单做了一个加法,而对 lst 来说是触发了 __iadd__ 魔术方法:

iadd of list

我们打印 id 检查一下理论的正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
l = []  # mutable
a = 2 # immutable

def f(lst):
lst += [1]
print(f"Inside f - lst ID after +=: {id(lst)}") # ID 保持不变!
print(f"Inside f - lst content: {lst}")

def g(i):
i += 1
print(f"Inside g - i ID after +=: {id(i)}") # ID 改变!
print(f"Inside g - i value: {i}")

# 函数外部
print(f"Outside - Before f call ID: {id(l)}")
f(l)
print(f"Outside - After f call ID: {id(l)}") # l 的 ID 保持不变
print(f"Outside - After f call list: {l}") # l 的内容已改变!

print("-" * 20)

print(f"Outside - Before g call ID: {id(a)}")
g(a)
print(f"Outside - After g call ID: {id(a)}") # a 的 ID 保持不变
print(f"Outside - After g call int: {a}") # a 的值保持不变

#>>>>> output: >>>>>
Outside - Before f call ID: 1701631187648
Inside f - lst ID after +=: 1701631187648
Inside f - lst content: [1]
Outside - After f call ID: 1701631187648
Outside - After f call list: [1]
--------------------
Outside - Before g call ID: 140735001078720
Inside g - i ID after +=: 140735001078752
Inside g - i value: 3
Outside - After g call ID: 140735001078720
Outside - After g call int: 2
  • 元组 (Tuple): 元组本身是不可变的。但是,元组的可哈希性取决于其包含的元素。如果元组中所有元素都是可哈希的(例如,整数、字符串、不可变元组等),那么该元组就是可哈希的。如果元组中包含任何不可哈希的元素(例如,列表、字典、集合),那么该元组就是不可哈希的。

    Python

    1
    2
    3
    4
    5
    hashable_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