为什么在 Python 中 hash(-1) == hash(-2)?
英文:https://omairmajid.com/posts/2021-07-16-why-is-hash-in-python
作者:Omair Majid
译者:豌豆花下猫&Claude-3.5-Sonnet
时间:原文发布于 2021.07.16,翻译于 2025.01.11
收录于:Python为什么系列 https://github.com/chinesehuazhou/python-whydo
当我在等待代码编译的时候,我在 Reddit 的 r/Python 上看到了这个问题:
等等,这是真的吗?
$ python
Python 3.9.6 (default, Jun 29 2021, 00:00:00)
[GCC 11.1.1 20210531 (Red Hat 11.1.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hash(-1)
-2
>>> hash(-2)
-2
>>> hash(-1) == hash(-2)
True
是的,确实如此。真让人惊讶!
让我们看看其它一些常见的哈希值:
>>> hash(1)
1
>>> hash(0)
0
>>> hash(3)
3
>>> hash(-4)
-4
看起来所有小整数的哈希值都等于它们自身,除了 -1
...
现在我完全被这个问题吸引住了。我试图自己找出答案。在接下来的内容中,我将带你了解如何自己寻找这个答案。
如何开始呢?什么能给我们一个权威的答案?
让我们看看源代码!Python 的实际实现代码!
获取源代码
我假设你和我一样,对 Python 的源代码在哪里完全没有概念。
那么,我们(假设从未看过 Python 的源代码)如何获取源代码来回答最初的问题呢?
也许我们可以用 Google?搜索 "python implementation" 会带来一些有趣的结果。
我搜索的 第一个结果 提到了 "CPython 参考实现"。
Github 上 Python 组织 的第二个仓库就是 "cpython"。这看起来很靠谱。我们如何确保万无一失呢?
我们可以访问 python.org。让我们去到源码下载页面。最终我找到了 Python 3.9.6 的压缩包。解压后,README.rst
也指向了 Github 上的 CPython。
好的,这就是我们的起点。让我们获取这些代码,以便后续搜索:
git clone https://github.com/python/cpython --depth 1
--depth 1
参数使 git
只获取有限的历史记录。这样可以让克隆操作快很多。如果之后需要完整的历史记录,我们可以再获取。
让我们深入研究
在研究代码时,我们需要找到一个起点。最好是容易搜索的东西,比如一个简单的字符串,不会有太多误导性的匹配。
也许我们可以使用 hash
函数的文档?我们可以用 help(hash)
来查看文档内容:
>>> hash
<built-in function hash>
>>> help(hash)
Help on built-in function hash in module builtins:
hash(obj, /)
Return the hash value for the given object.
Two objects that compare equal must also have the same hash value, but the
reverse is not necessarily true.
现在,我们可以用它来找到 hash()
的实现:
$ grep -r 'Return the hash value'
Python/clinic/bltinmodule.c.h:"Return the hash value for the given object.\n"
Python/bltinmodule.c:Return the hash value for the given object.
Doc/library/functions.rst: Return the hash value of the object (if it has one). Hash values are
Lib/hmac.py: """Return the hash value of this hashing object.
hmac
可能与加密的 HMAC 实现有关,所以我们可以忽略它。functions.rst
是一个文档文件,所以也可以忽略。
Python/bltinmodule.c
看起来很有趣。如果我们查看这个文件,会找到这样一段代码:
/*
...
Return the hash value for the given object.
Two objects that compare equal must also have the same hash value, but the
reverse is not necessarily true.
[clinic start generated code]*/
static PyObject *
builtin_hash(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=237668e9d7688db7 input=58c48be822bf9c54]*/
{
Py_hash_t x;
x = PyObject_Hash(obj);
if (x == -1)
return NULL;
return PyLong_FromSsize_t(x);
}
搜索 PyLong
带我来到这里。看起来 PyLongObject
是 Python 整数的原生表示(这在稍后会派上用场)。在浏览了 PyLongObject
文档并重读这段代码后,看起来是这样的:
- 我们调用
PyObject_Hash
来获得一个对象的哈希值 - 如果计算出的哈希值是 -1,那表示是一个错误
- 看起来我们用 -1 来表示错误,所以没有哈希函数会为真实对象计算出 -1
- 我们将
Py_Ssize_t
转换为PyLongObject
(文档中称之为:"这是 PyObject 的子类型,表示一个 Python 整数对象")
啊哈!这就解释了为什么 hash(0)
是 0
,hash(1)
是 1
,hash(-2)
是 -2
,但 hash(-1)
不是 -1
。这是因为 -1
在内部被用来表示错误。
但为什么 hash(-1)
是 -2
呢?是什么将它设置成了这个值?
让我们看看能否找出原因。
我们可以先查找 PyObject_Hash
。让我们搜索一下。
$ ag PyObject_Hash
...
Objects/rangeobject.c
552: result = PyObject_Hash(t);
Objects/object.c
777:PyObject_HashNotImplemented(PyObject *v)
785:PyObject_Hash(PyObject *v)
802: return PyObject_HashNotImplemented(v);
Objects/classobject.c
307: y = PyObject_Hash(a->im_func);
538: y = PyObject_Hash(PyInstanceMethod_GET_FUNCTION(self));
...
虽然有很多干扰,但唯一的实现似乎在 Objects/object.c
中:
Py_hash_t
PyObject_Hash(PyObject *v)
{
PyTypeObject *tp = Py_TYPE(v);
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
/* 为了保持通用做法:在 C 代码中仅从 object 继承的类型,应该无需显式调用 PyType_Ready 就能工作,
* 我们在这里隐式调用 PyType_Ready,然后再次检查 tp_hash 槽
*/
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
return -1;
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
}
/* Otherwise, the object can't be hashed */
return PyObject_HashNotImplemented(v);
}
这段代码相当令人困惑。幸运的是,注释很清晰。在多次阅读后,似乎这段代码——考虑到类型的一些延迟加载(?)——先找到对象的类型(使用 Py_TYPE
)。然后寻找该类型的 tp_hash
函数,并在 v 上调用该函数:(*tp->tp_hash)(v)
我们在哪里能找到 -1
的 tp_hash
呢?让我们再次搜索 tp_hash
:
$ ag tp_hash -l
...
Modules/_multiprocessing/semaphore.c
Objects/sliceobject.c
Objects/moduleobject.c
Objects/exceptions.c
Modules/_pickle.c
Objects/frameobject.c
Objects/setobject.c
Objects/rangeobject.c
Objects/longobject.c
Objects/object.c
Objects/methodobject.c
Objects/classobject.c
Objects/enumobject.c
Objects/odictobject.c
Objects/complexobject.c
...
这是一个很长的列表。回想一下文档中关于 PyLongObject
的说明("这个...表示一个 Python 整数对象"),我先查看下 Objects/longobject.c
:
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
long_to_decimal_string, /* tp_repr */
&long_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)long_hash, /* tp_hash */
...
所以 PyLongObject
类型对象的 tp_hash
是 long_hash
。让我们看看这个函数。
static Py_hash_t
long_hash(PyLongObject *v)
{
Py_uhash_t x;
Py_ssize_t i;
int sign;
...
if (x == (Py_uhash_t)-1)
x = (Py_uhash_t)-2;
return (Py_hash_t)x;
}
注意我删除了大部分实现细节。但这个函数的结尾正好符合我们的预期:-1
被保留用作错误信号,所以代码明确地将该返回值转换为 -2
!
这就解释了为什么 hash(-1)
最终与 hash(-2)
相同。这不是一个彩蛋,只是为了避免使用 -1
作为 hash()
方法的返回值,因此采取的变通方法。
这是正确/完整的答案吗?
如前所述,我从未看过 Python 代码库。我认为自己找到了答案。但这是对的吗?我可能完全错了。
幸运的是,/u/ExoticMandibles 在 Reddit 帖子中提供了答案:
Python 的参考实现是 "CPython",这很可能就是你正在使用的 Python。CPython 是用 C 语言编写的,与 Python 不同,C 语言没有异常处理。所以,在 C 语言中,当你设计一个函数,并且想要表示"发生了错误"时,必须通过返回值来表示这个错误。
CPython 中的 hash() 函数可能返回错误,所以它定义返回值 -1 表示"发生了错误"。但如果哈希计算正确,而对象的实际哈希值恰好是 -1,这可能会造成混淆。所以约定是:如果哈希计算成功,并得到值是 -1,就返回 -2。
在 CPython 中,整数("长整型对象")的哈希函数中有专门的代码来处理这种情况:
https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967
这正是我通过阅读代码推测出的结果。
结论
我从一个看似难以回答的问题开始。但是通过几分钟的代码探索——Python 整洁的代码库使得查看它的代码比我见过的其它代码库要容易得多——很容易就发现和理解了答案!如果你接触过计算机,这应该不足为奇。这里没有魔法,只有层层的抽象和代码。
如果本文有什么启示的话,那就是:查看源代码! (文档可能会过时,注释可能不准确,但源码是永恒的。)
为什么在 Python 中 hash(-1) == hash(-2)?的更多相关文章
- Python中hash加密
目录 简介 概念 特点 hash有哪些 算法碰撞 加盐防碰撞 加密 hashlib 主要方法 特有方法 使用方法 加盐 crypt 主要方法 使用说明 应用 密码加密 应用一致性校验 简介 概念 散列 ...
- 第二百九十六节,python操作redis缓存-Hash哈希类型,可以理解为字典类型
第二百九十六节,python操作redis缓存-Hash哈希类型,可以理解为字典类型 Hash操作,redis中Hash在内存中的存储格式如下图: hset(name, key, value)name ...
- windows中抓取hash小结(下)
书接上回,windows中抓取hash小结(上) 指路链接 https://www.cnblogs.com/lcxblogs/p/13957899.html 继续 0x03 从ntds.dit中抓取 ...
- MySQL索引的Index method中btree和hash的优缺点
MySQL索引的Index method中btree和hash的区别 在MySQL中,大多数索引(如 PRIMARY KEY,UNIQUE,INDEX和FULLTEXT)都是在BTREE中存储,但使用 ...
- Jedis中的一致性hash
Jedis中的一致性hash 本文仅供大家参考,不保证正确性,有问题请及时指出 一致性hash就不多说了,网上有很多说的很好的文章,这里说说Jedis中的Shard是如何使用一致性hash的,也为大家 ...
- PHP中Array的hash函数实现
PHP中使用最多的非Array莫属了,那Array是如何实现的? 在PHP内部Array通过一个hashtable来实现,其中使用链接法解决hash冲突的问题,这样最坏情况下,查找Array元素的复杂 ...
- 记一次诡异的bug调试——————关于JDK1.7和JDK1.8中HashSet的hash(key)算法的区别
现象: 测试提了一个bug,我完全复现不了,但是最吊诡的是在其他人的机器上都可以复现.起初以为是SVN合并后出现的冲突,后来经过对比法排查: step 1: 我本地开两个jetty,一个跑合并之前的版 ...
- hadoop Partiton中的字符串Hash函数改进
最近的MapReduce端的Partition根据map生成的Key来进行哈希,导致哈希出来的Reduce端处理任务数量非常不均匀,有些Reduce端处理的数据量非常小(几分钟就执行完成,而最后的pa ...
- jedis中的一致性hash算法
[http://my.oschina.net/u/866190/blog/192286] jredis是redis的java客户端,通过sharde实现负载路由,一直很好奇jredis的sharde如 ...
- JDK1.8中HashMap的hash算法和寻址算法
JDK 1.8 中 HashMap 的 hash 算法和寻址算法 HashMap 源码 hash() 方法 static final int hash(Object key) { int h; ret ...
随机推荐
- PHP、JS、css、python、mysql 基本常用函数特殊方法记录
html <a draggable="false">禁止拖拽</a> css .nowrap{word-break:keep-all;white-space ...
- C++之OpenCV入门到提高004:Mat 对象的使用
一.介绍 今天是这个系列<C++之 Opencv 入门到提高>得第四篇文章.这篇文章很简单,介绍如何使用 Mat 对象来实例化图像实例,了解它的构造函数和常用的方法,这是基础,为以后的学习 ...
- python之执行shell命令
python 执行shell命令,且执行完后将shell端的输出返回 subprocess import subprocess # 要执行的命令 command = "ls -lrt&quo ...
- ubuntu卸载php8后在命令行终端上面还是显示8的版本
使用apt install了php8然后卸载后发现php -v还是8的版本,找来找去,最后发现是需要卸载sudo apt remove php8.0-cli才行 然后使用 sudo apt autor ...
- 批量归一化(BN, Batch Normalization)
现在的神经网络通常都特别深,在输出层向输入层传播导数的过程中,梯度很容易被激活函数或是权重以指数级的规模缩小或放大,从而产生"梯度消失"或"梯度爆炸"的现象,造 ...
- 关于ClassLoader中getResource与getResourceAsStream的疑问
背景: 某日临近下班,一个同事欲任何类中获取项目绝对路径,不通过Request方式获取,可是始终获取不到预想的路径.于是晚上回家google了一下,误以为是System.getProperty(&qu ...
- Java通用分页
一. 要分页我们必须要有数据库,所以我们先准备下数据库,其数据库脚步如下: --以下是创建数据库和数据库表以及向数据库插入数据 use master Go if exists(select ...
- nginx之日志
1)耗时问题定位 这几天在优化服务器的响应时间,在根据 nginx 的 accesslog 中 requesttime进行程序优化时,发现有个接口,直接返回数据,平均的requesttime进行程序优 ...
- yum之镜像加速
有没有遇到使用yum安装软件慢如龟,默认的系统使用的是centos的镜像源,我们可以修改为国内镜像源加速软件安装 163)http://mirrors.163.com/.help/centos.htm ...
- php-fpm常见错误
1. WARNING: Nothing matches the include pattern '/usr/local/php7/etc/php-fpm.d/*.conf' # cd /usr/loc ...