字典(dict
)类型在 Python 基础数据类型中有着举足轻重的地位,日常编码中几乎离不开字典的使用。不过字典中有一个看似非常奇怪的现象,常被人忽略。本篇文章就来探索这一奇异现象。
请用 5 秒钟思考一下,下面这个字典表达式会输出什么?
1 | True: 'True', 1: '1', 1.0: '1.0'} { |
将以上字典表达式复制到 Python 控制台中执行,将会得到如下结果:
1 | {True: '1.0'} |
初次见到这个结果时我是非常吃惊的,what ?这看起来似乎并不符合常理,但当我们一步一步分析出背后的原因之后,就不会对这个结果感到奇怪了。
我们可以将上面的字典表达式拆解为多个步骤来分析。
1 | d = dict() |
首先,先创建一个空的字典对象,然后依次给字典赋值,最终得到字典结果为 {True: '1.0'}
。
从以上代码的执行步骤和结果,我们可以分析出,在 Python 中字典会将作为键的 True
、1
和 1.0
认为是 相等
的。我们可以在 Python 控制台得到验证。
1 | True == 1 == 1.0 |
果然,这三个对象对 python 来说是 相等
的。实际上,在 Python 中,bool
类型继承自 int
类型,所以这三个对象 相等
也是合理的。
Python 提供了 issubclass
函数可以判断一个类型是否是另一个类型的子类,使用这个函数来验证 bool
类型是 int
类型的子类。
1 | issubclass(bool, int) |
还可以使用 isinstance
函数来判断一个对象是否是一个类型的实例,由于 True
是 bool
类型的实例,那么 True
自然也是 int
类型的实例对象。
1 | True, int) isinstance( |
我们证实了 True
、1
和 1.0
是 相等
的对象。但这还不足以说明问题,在 Python 中两个对象的值 相等
并不能说明它们放到字典中时会变为同一个键。这点通过稍后的示例就会明白。
在 Python 中,一个对象是否可以作为字典的键是有要求的,我们知道只有不可变类型才能作为字典的键。实际上 Python 判断一个对象是否可以作为字典的键,其实是判断这个对象是否为 可哈希
(hashable
)对象。
Python 在 官网文档 中对 可哈希
对象进行了说明。大概意思是说:如果一个对象的 hash
值在其生命周期内不会改变(需要实现 __hash__()
方法),并且这个对象可以与其他对象进行比较(需要实现 __eq__()
方法),则该对象是 可哈希
的。由于 Python 内置的常见不可变类型都实现了 __hash__()
方法 和 __eq__()
方法,所以它们都是 可哈希
的。
这里还要强调一点:如果两个 可哈希
对象的值是相等的,那么它们的 hash
值也必然是相等的。这是 Python 的规范,我们自定义的类型也要遵循这个规范。
说的直白一些,Python 字典的键是不可重复的,而 Python 在操作字典的键时,如果两个对象的 hash
值相同,并且这两个对象的值也 相等
,那么这两个对象会被当作同一个键。
Python 提供了 hash
函数可以获得一个对象的 hash
值,它会自动调用对象的 __hash__()
方法。至于判断两个对象的值是否相等,实际上就是使用 ==
运算符,它会自动调用对象的 __eq__()
方法。
知道了有关字典的键的特性,接下来我们自己实现一个类,通过三个示例分别对三种不同的情况进行探索,从而通过实际的代码来验证字典的键的特性。
(以下示例代码中定义的 __init__
方法和 __repr__
方法只用于辅助观察,并不会对结果产生影响。)
示例一:
1 | class A(object): |
由示例一可知,a1
和 a2
两个对象的值相等,但 hash
值不同,最终得到的字典并不会将这两个对象看作同一个键。
示例二:
1 | class A(object): |
由示例二可知,a1
和 a2
两个对象的 hash
值相同,但值不相等,最终得到的字典也不会将这两个对象看作同一个键。
示例三:
1 | class A(object): |
由示例三可知,a1
和 a2
两个对象的 hash
值相同,并且值也相等,最终得到的字典会将这两个对象看作同一个键。
通过以上三个示例代码的演示,想必不用我多说,你一定已经猜测到了,实际上在 Python 中,True
、1
和 1.0
这三个对象的 hash
值也是相同的。
1 | True), hash(1), hash(1.0) hash( |
至此,关于 Python 字典中的 奇异
现象也就解释通了,下次再见到同样的问题就不会觉得奇怪了。