hash('hello')-4938767993309476271

假设你要在图书馆里找一本书。如果所有书都随机堆在一起,你得一本一本翻——运气不好要翻遍整个图书馆。但如果图书馆按颜色把书分到不同隔间(红色的放A区、蓝色的放B区……),你要找红皮书就直接去A区找,快得多。
这个“按某个规则把东西分到不同格子”的过程,就是哈希(hash)。
在计算机中,哈希函数将任意大小的数据(字符串、数字、任何对象)映射到一个固定长度的整数,这个整数称为哈希值(hash value)。
hash('hello')-4938767993309476271
hash(42)42
hash((1, 2, 3))529344067295497451
哈希值有两个重要特点:
hash() 返回相同的整数。不同的对象可能产生相同的哈希值,称为哈希碰撞。好的哈希函数会把碰撞概率降到很低,但不能完全避免。
Python 默认启用哈希随机化。同一个字符串在单个进程内的哈希值相同,但在不同进程中的哈希值可能不同。这是为了防止恶意构造哈希碰撞的攻击。
Python中有的对象是可变的(mutable),可变对象的值可以被改变。
有的对象不可变(immutable)。原则上来说,不可变对象一经创建,其值不可被改变。
对象的可变性,由其类型决定。
如果一个对象的(哈希)值在其生命周期中不会改变,这个对象就是可哈希的(hashable)。可哈希对象可以用作字典的键,也可以作为集合的元素。
hash() 函数可以查看一个对象的哈希值:
# 不可变类型通常是可哈希的
hash(42), hash(3.14), hash('hello'), hash((1, 2, 3))(42, 322818021289917443, -4938767993309476271, 529344067295497451)
# 可变类型是不可哈希的
hash([1, 2, 3])--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[5], line 2 1 # 可变类型是不可哈希的 ----> 2 hash([1, 2, 3]) TypeError: unhashable type: 'list'
一个常见的误解是「不可变 = 可哈希」。 实际上,不可变容器只有在它的所有元素都可哈希的时候,才是可哈希的: 判断一个对象是否可哈希,最简单的方法是看 hash() 函数是否报错。
# tuple 的元素全部不可变,可哈希
t1 = (1, 2, 3)
hash(t1)529344067295497451
# tuple 包含可变元素,不可哈希
t2 = (1, 2, [3, 4])
hash(t2)--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[7], line 3 1 # tuple 包含可变元素,不可哈希 2 t2 = (1, 2, [3, 4]) ----> 3 hash(t2) TypeError: unhashable type: 'list'
字典的底层实现是哈希表(hash table)。当你执行 d[key] = value 时,Python 做了两件事:
key 的哈希值,决定这个键值对放在哪个“格子”(bucket)里当你查找 d[key] 时,Python 再次计算 key 的哈希值,直接去对应的格子里找,而不需要遍历所有键。这就是字典查找接近 O(1) 的原因。
现在,假设字典的键是可变的:
[1, 2] 作为键存入字典,它的哈希值决定了它所在的格子[1, 2].append(3)# 假如用列表做字典键
d = {[1, 2]: 'value'}--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[8], line 2 1 # 假如用列表做字典键 ----> 2 d = {[1, 2]: 'value'} TypeError: unhashable type: 'list'
集合相当于只有键没有值的字典,因此集合元素也必须是可哈希的。
可变对象的哈希值会随着自身的变化而改变,所以不能做字典的键或集合的元素。可哈希的本质要求是:对象的身份和值必须终身绑定,不能变。
| 类型 | 可变 | 可哈希 | 说明 |
|---|---|---|---|
| int, float, bool, complex | 否 | 是 | 不可变值类型 |
| str | 否 | 是 | 不可变文本 |
| tuple | 否 | 是 | 仅当所有元素都可哈希时 |
| frozenset | 否 | 是 | 不可变集合 |
| list | 是 | 否 | 可变容器 |
| dict | 是 | 否 | 键必须可哈希 |
| set | 是 | 否 | 元素必须可哈希 |
| bytearray | 是 | 否 | 可变字节序列 |
a = b形式的直接赋值,只是给同一个对象多贴了一个标签。通过任何一个标签修改对象,所有标签看到的内容都会变:
a = [1, 2, 3]
b = a # 直接赋值:b 和 a 指向同一个对象
id(a) == id(b)True
b.append(4)
a[1, 2, 3, 4]
用copy模块的copy()方法可进行浅拷贝:创建一个新的容器对象,但容器内部的元素仍然是原来那些对象的引用(id 相同)。
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a) # 浅拷贝
a is b # 不同容器
a[0] is b[0] # 但内部的子列表是同一个对象False
True
由于内部元素共享,修改嵌套的可变对象会影响两个容器:
b[0].append(100)
b[0], a[0] # a[0],b[0]都被改了
a # a 也变了([1, 2, 100], [1, 2, 100])
[[1, 2, 100], [3, 4]]
但如果给外层容器的某个位置重新赋值(绑定新对象),则不会影响另一个容器:
b[1] = [300, 400] # b[1] 绑定了新列表
a, b # a 不受影响([[1, 2, 100], [3, 4]], [[1, 2, 100], [300, 400]])
深拷贝创建完全独立的副本——新的容器,新的内部可变对象。
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
id(a) == id(b) # False 不同容器
id(a[0]) == id(b[0]) # False 内部子列表也是独立对象False
False
b[0].append(100)
b[0], a[0] # b[0] 变了,a[0] 不变([1, 2, 100], [1, 2])
深拷贝更安全(修改副本不会影响原对象),但更费时间和内存,因为需要复制所有嵌套的可变对象。
id() 返回对象的身份标识(可理解为对象在内存中的地址)。两个变量如果 id() 相同,说明它们指向同一个对象。
与之对应,is 判断两个变量是否指向同一个对象(即 id() 相同),== 判断两个对象的值是否相等:
a = [1, 2, 3]
b = a
c = [1, 2, 3]
a is b # True:a 和 b 是同一个对象
a == c # True:值相等
a is c # False:不同对象True
True
False
对可变对象,我们可以修改对象本身,也可以通过赋值让变量绑定一个新对象,这两者的 id 变化截然不同。
# 修改对象本身(id 不变)
lst = [1, 2, 3]
print(f'原列表{lst=}, 原{id(lst)=}')
lst.append(4)
print(f'现列表{lst=}, 现{id(lst)=}')原列表lst=[1, 2, 3], 原id(lst)=2627856912128
现列表lst=[1, 2, 3, 4], 现id(lst)=2627856912128
# 变量重新绑定(id 改变)
lst = [1, 2, 3]
print(f'原{id(lst)=}')
lst = [4, 5, 6]
print(f'现{id(lst)=}')原id(lst)=2627857409792
现id(lst)=2627856912128
对不可变对象,你无法修改对象本身。任何看似“修改”的操作,实际上都在创建新对象。
x = 5
print(f'原{x=},原{id(x)=}')
x = x + 1 # 5 + 1 创建了一个新整数 6
print(f'现{x=},现{id(x)=}')原x=5,原id(x)=140733904696360
现x=6,现id(x)=140733904696392
s = 'hello'
print(f'原{s=},原{id(s)=}')
s = s + ' world' # 创建了新字符串
print(f'现{s=},现{id(s)=}')原s='hello',原id(s)=2627730060656
现s='hello world',现id(s)=2627750450736
对于可变对象,+=原地修改;对于不可变对象,+=创建新对象。
# += 原地修改可变对象
a = [1, 2]
print(f'+=之前,{a=},{id(a)=}')
a += [3, 4]
print(f'+=之后,{a=},{id(a)=}')+=之前,a=[1, 2],id(a)=2627857413504
+=之后,a=[1, 2, 3, 4],id(a)=2627857413504
# += 创建新的不可变对象
a = (1, 2)
print(f'+=之前,{a=},{id(a)=}')
a += (3, 4)
print(f'+=之后,{a=},{id(a)=}')+=之前,a=(1, 2),id(a)=2627857225920
+=之后,a=(1, 2, 3, 4),id(a)=2627857172496
元组本身不可变——你不能增删改它的元素引用。但如果元组里的某个元素恰好是可变对象,这个可变对象的值可以被修改。
t = (1, 2, [3, 4])
t[2].append(5)
t # 元组的值变了!(1, 2, [3, 4, 5])
严格来说,元组的「不可变」指的是:元组中每个元素的引用(id)不可改变。但如果被引用的对象本身是可变对象,这个可变对象内部的改变不受元组约束。
# 重新绑定元素不被允许
t[2] = [3, 4, 5]--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[24], line 2 1 # 重新绑定元素不被允许 ----> 2 t[2] = [3, 4, 5] TypeError: 'tuple' object does not support item assignment
理解这一点对理解“可哈希”很重要:如果一个 tuple 包含可变对象,它的哈希值实际上可以改变(因为内部元素变了),因此这样的 tuple 不可哈希——这正是 hash((1, 2, [3, 4])) 会报错的原因。
函数默认参数只在定义时计算一次,而不是每次调用时重新计算。
def append_to_list(value, lst=[]):
lst.append(value)
return lst
append_to_list(1) # [1]
append_to_list(2) # [1, 2] !不是 [2]
append_to_list(3) # [1, 2, 3][1]
[1, 2]
[1, 2, 3]
正确的做法是用 None 做默认值,在函数内部创建新列表:
def append_to_list(value, lst=None):
if lst is None:
lst = []
lst.append(value)
return lstPython对小整数有特殊优化:小整数的哈希值通常是其自身。↩︎