解读Python中字典的key都可以是什么

更新时间:2022-09-28 10:02:04

一个对象能不能作为字典的key,就取决于其有没有__hash__方法。所以所有python自带类型中,除了list、dict、set和内部至少带有上述三种类型之一的tuple之外,其余的对象都能当key。

比如数值/字符串/完全不可变的元祖/函数(内建或自定义)/类(内建或自定义)/方法/包等等你能拿出手的,不过有的实际意义不高。还有数值型要注意,因为两个不同的相等数字可以有相同的哈希值,比如1和1.0。

解释

代码版本:3.6.3;文档版本:3.6.6

Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can’t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append()and extend().

字典的键可以是任意不可变类型,需要注意的是tuple元组作为键时,其中不能以任何方式包含可变对象。

那。。到底什么样的是不可变类型呢?不可能给对象专门标注一个属性是可变类型还是不可变类型啊,这没有任何其他意义,一定是通过其他途径实现的。把list当做键试一下

a = [1, 2, 3]
d = {a: a}
 
 
# 第二行报错:
# TypeError: unhashable type: 'list'

报错说list类型是不可哈希的,噢,原来是靠能不能hash来判断的,另外文档下面接着说同一字典中每个键都是唯一的,正好每个对象的哈希值也是唯一的,对应的很好。

It is best to think of a dictionary as an unordered set of key: value pairs, with the requirement that the keys are unique (within one dictionary).

查看源代码可以看到object对象是定义了__hash__方法的,

而list、set和dict都把__hash__赋值为None了

# 部分源码
 
class object:
    """ The most base type """
 
    def __hash__(self, *args, **kwargs):  # real signature unknown
        """ Return hash(self). """
        pass
 
 
class list(object):
    __hash__ = None
 
 
class set(object):
    __hash__ = None
 
 
class dict(object):
    __hash__ = None

那这样的话。。。我给他加一个hash不就能当字典的key了,key不就是可变的了。

注意

此处只是我跟着想法随便试,真的应用场景不要用可变类型作为字典的key。

class MyList(list):
    """比普通的list多一个__hash__方法"""
 
    def __hash__(self):
        # 不能返回hash(self)
        # hash(self)会调用self的本方法,再调用回去,那就没完了(RecursionError)
        # 用的时候要注意实例中至少有一个元素,不然0怎么取(IndexError)
        return hash(self[0])
 
 
l1 = MyList([1, 2])  # print(l1) -> [1, 2]
d = {l1: 'Can?'}
print(d)  # -->  {[1, 2]: 'Can?'}
l1.append(3)
print(d)  # {[1, 2, 3]: 'Can?'}
print(d[l1])  # -->  Can?

到这里就可以肯定的说,一个对象能不能作为字典的key,就取决于其有没有__hash__方法。所以所有python自带类型中,目前我已知的除了list、dict、set和内部带有以上三种类型的tuple之外,其余的对象都能当key。而我们自己定义的类,一般情况下都直接间接的和object有关,都带有__hash__方法。

另外我想到,既然字典的键是唯一的,而哈希值也是唯一的,这么巧,键的唯一性不会就是用哈希值来确定的吧?我上一个例子中__hash__方法返回的是0号元素的哈希值,那我直接用相同哈希值的对象是不是就能改变那本来不属于它的字典值呢?

class MyList(list):
    def __hash__(self):
        return hash(self[0])
 
 
l1 = MyList([1, 2])  # print(l1) -> [1, 2]
d = {}
d[l1] = l1
print(d)  # {[1, 2]: [1, 2]}
d[1] = 1
print(d)  # {[1, 2]: [1, 2], 1: 1}

竟然没有改成功而是新添加了一个键值对,可self[0]就是1啊,哈希值一样啊,怎么会不一样呢?难道要键的值一样才能判断是同一个键吗?重写__eq__方法试一下。

class MyList(list):
    def __hash__(self):
        return hash(self[0])
 
    def __eq__(self, other):
        return self[0] == other
 
 
l1 = MyList([1, 2])  # print(l1) -> [1, 2]
d = {}
d[l1] = l1
print(d)  # {[1, 2]: [1, 2]}
d[1] = 1
print(d)  # {[1, 2]: 1}

这回成功了,那就是__hash__返回值相等,且eq判断也相等,才会被认为是同一个键。那这两个先判断哪个呢?加个代码试一下

class MyList(list):
    def __hash__(self):
        print('hash is run')
        return hash(self[0])
 
    def __eq__(self, other):
        print('eq is run')
        return self[0] == other
 
 
l1 = MyList([1, 2])  # print(l1) -> [1, 2]
d = {}
d[1] = 1
d[l1] = 'l1'
print(d)
 
 
# 结果:
# hash is run
# eq is run
# {1: 'l1'}

__hash__先执行,另外字典在内存中存储数据的位置和键的hash也是有关的,逻辑上也像印证。

先计算hash,找到相对应的那片内存空间,里面没有值的话就直接写入,对于字典来说就是新增键值对;如果里面已经有值了,那就判断新来的键和原来的那里的键是不是相等,相等就认为是一个键,对于字典来说就是更新值,不相等就再开空间,相当于字典新增键值对。

在你验证自己想法的时候可能遇到__hash__和__eq__的一些想不到的麻烦,可以看这里:__hash__和__eq__的继承使用问题

Python