网易首页 > 网易号 > 正文 申请入驻

为什么在 Python 中 hash(-1) == hash(-2)?

0
分享至

△△请给“Python猫”加星标 ,以免错过文章推送


英文:https://omairmajid.com/posts/2021-07-16-why-is-hash-in-python

作者:Omair Majid

译者:豌豆花下猫&Claude-3.5-Sonnet

时间:原文发布于 2021.07.16,翻译于 2025.01.11

当我在等待代码编译[1]的时候,我在 Reddit 的 r/Python 上看到了这个问题:

hash(-1) == hash(-2) 是个彩蛋吗?[2]

等等,这是真的吗?

$ 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" 会带来一些有趣的结果。

我搜索的第一个结果[3]提到了 "CPython 参考实现"。

Github 上Python 组织[4]的第二个仓库就是 "cpython"。这看起来很靠谱。我们如何确保万无一失呢?

我们可以访问 python.org。让我们去到源码下载页面。最终我找到了Python 3.9.6 的压缩包[5]。解压后,README.rst也指向了 Github 上的 CPython。

好的,这就是我们的起点。让我们获取这些代码,以便后续搜索:

git clone https://github.com/python/cpython --depth 1

--depth 1参数使git只获取有限的历史记录。这样可以让克隆操作快很多。如果之后需要完整的历史记录,我们可以再获取。

让我们深入研究

在研究代码时,我们需要找到一个起点。最好是容易搜索的东西,比如一个简单的字符串,不会有太多误导性的匹配。

也许我们可以使用hash函数的文档?我们可以用help(hash)来查看文档内容:

>>> hash

      
 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带我来到这里[6]。看起来PyLongObject是 Python 整数的原生表示(这在稍后会派上用场)。在浏览了PyLongObject文档并重读这段代码后,看起来是这样的:

  1. 我们调用PyObject_Hash来获得一个对象的哈希值

  2. 如果计算出的哈希值是 -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 帖子中提供了答案[7]:

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[8]

这正是我通过阅读代码推测出的结果。

结论

我从一个看似难以回答的问题开始。但是通过几分钟的代码探索——Python 整洁的代码库使得查看它的代码比我见过的其它代码库要容易得多——很容易就发现和理解了答案!如果你接触过计算机,这应该不足为奇。这里没有魔法,只有层层的抽象和代码。

如果本文有什么启示的话,那就是:查看源代码![9](文档可能会过时,注释可能不准确,但源码是永恒的。)

参考资料

等待代码编译: https://xkcd.com/303/

hash(-1) == hash(-2) 是个彩蛋吗?: https://www.reddit.com/r/Python/comments/oks5km/is_hash_1hash2_an_easter_egg/

第一个结果: https://wiki.python.org/moin/PythonImplementations

Python 组织: https://github.com/python

[5]

Python 3.9.6 的压缩包 : https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz

[6]

这里: https://docs.python.org/3/c-api/long.html

[7]

/u/ExoticMandibles 在 Reddit 帖子中提供了答案: https://www.reddit.com/r/Python/comments/oks5km/is_hash_1hash2_an_easter_egg/h5a7ylc/

[8]

https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967: https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967

[9]

查看源代码!: https://wiki.c2.com/?UseTheSourceLuke

如果你正在寻找优质的Python文章和项目,我必须向你推荐Python潮流周刊!

它精选全网的优秀文章、教程、开源项目、软件工具、播客、视频、热门话题等丰富内容,让你紧跟技术最前沿,获取最新的第一手学习资料!

欢迎点击下方图片,了解这份全世界知识密度最高、知识广度最大的 Python 技术周刊。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
洪秀柱曾爆猛料,称若民进党胜选,很怀疑台湾地区领导人是美国人

洪秀柱曾爆猛料,称若民进党胜选,很怀疑台湾地区领导人是美国人

白色得季节
2026-09-28 19:29:12
日本防卫大臣狂言“不设禁忌讨论核武器和核潜艇”,外交部:“拥有核武器”不是日方想不想讨论的问题,没有讨价还价的余地

日本防卫大臣狂言“不设禁忌讨论核武器和核潜艇”,外交部:“拥有核武器”不是日方想不想讨论的问题,没有讨价还价的余地

每日经济新闻
2026-10-01 11:17:46
曹轶欧回忆:1933年康生亲手用斧头劈死了顾顺章家属!

曹轶欧回忆:1933年康生亲手用斧头劈死了顾顺章家属!

喜喜江江
2026-09-29 07:25:06
暴雨大暴雨,局地降温10℃以上!新一轮降温降雨来袭,昆明→

暴雨大暴雨,局地降温10℃以上!新一轮降温降雨来袭,昆明→

掌上春城
2026-10-01 21:35:01
知名投资人曝刘欢真正死因!大家都猜错了,8月他在餐厅状态不错

知名投资人曝刘欢真正死因!大家都猜错了,8月他在餐厅状态不错

笑饮孤鸿非
2026-09-29 03:04:49
钱再多又如何?52岁刘强东手握千亿身家,儿子成了一生的遗憾

钱再多又如何?52岁刘强东手握千亿身家,儿子成了一生的遗憾

莫地方
2026-08-23 19:19:07
1948年,陈毅曾在一份内部报告中坦言:单论个人军事素质,华野基层的许多连长不如国军普通士兵

1948年,陈毅曾在一份内部报告中坦言:单论个人军事素质,华野基层的许多连长不如国军普通士兵

饭小妹说历史
2026-09-30 09:31:41
吴奇隆时隔35年重返天安门庆国庆!笑容满面复刻当年小虎队合影

吴奇隆时隔35年重返天安门庆国庆!笑容满面复刻当年小虎队合影

韩小娱
2026-10-01 21:04:27
外国人对中国人“一天吃三顿”感到震惊,这反映了哪些饮食文化差异?

外国人对中国人“一天吃三顿”感到震惊,这反映了哪些饮食文化差异?

向航说
2026-09-17 12:49:26
开拓者对内线新援的能力非常认可,他能胜任空间型大前锋的角色?

开拓者对内线新援的能力非常认可,他能胜任空间型大前锋的角色?

稻谷与小麦
2026-10-02 00:05:13
一夫一妻制逐渐难维持!社会学家分析:未来3种模式将成为主流

一夫一妻制逐渐难维持!社会学家分析:未来3种模式将成为主流

王二哥老搞笑
2026-09-24 07:26:29
开局之年看中国·潮涌荆楚|如果信念有颜色,那一定是中国红

开局之年看中国·潮涌荆楚|如果信念有颜色,那一定是中国红

极目新闻
2026-10-01 17:24:08
有人提出如果粤语是秦军队带过来,陕西人应能讲粤语,评论爆了

有人提出如果粤语是秦军队带过来,陕西人应能讲粤语,评论爆了

文章创新
2026-09-29 07:09:34
男不男女不女!穿裙子露大腿的周深,鸟巢献唱才2天恶心一幕发生

男不男女不女!穿裙子露大腿的周深,鸟巢献唱才2天恶心一幕发生

林轻吟
2026-10-01 07:15:56
伊朗:已收到美国对伊方有关结束战事提议的回应

伊朗:已收到美国对伊方有关结束战事提议的回应

新京报
2026-09-30 17:03:14
特朗普刚放话卖中国武器,转头就否认:我没说过!日本吓出一身汗

特朗普刚放话卖中国武器,转头就否认:我没说过!日本吓出一身汗

启迪你的思维
2026-10-01 23:42:21
夸雷斯马谈C罗离队:认可历史地位,但葡萄牙的球衣没人能凌驾!

夸雷斯马谈C罗离队:认可历史地位,但葡萄牙的球衣没人能凌驾!

体育闲话说
2026-10-01 09:19:30
《余红旧事》口碑大爆,本是冲马伊琍来的,却被40岁跨界歌手惊艳

《余红旧事》口碑大爆,本是冲马伊琍来的,却被40岁跨界歌手惊艳

寒士之言本尊
2026-09-30 15:30:44
9名中国公民在埃及阿斯旺省交通事故中受伤

9名中国公民在埃及阿斯旺省交通事故中受伤

新华社
2026-10-01 19:33:08
安踏,快被自己养大的“小弟”挤下主位了!

安踏,快被自己养大的“小弟”挤下主位了!

可乐爱微笑
2026-09-03 22:56:06
2026-10-02 00:24:49
Python猫 incentive-icons
Python猫
人生苦短,我用Python。博客:https://pythoncat.top
758文章数 8122关注度
往期回顾 全部

科技要闻

5999元起!华为Mate 90系列发布

头条要闻

美国一女囚犯被执行死刑 注射两剂药物后其"鼾声大作"

头条要闻

美国一女囚犯被执行死刑 注射两剂药物后其"鼾声大作"

体育要闻

“今天的表现,我们可以昂首离开球场”

娱乐要闻

奚梦瑶晒四太豪礼!22只龙凤镯近300万

财经要闻

智谱发上亿Token 能挽回开发者信任吗?

汽车要闻

2027款极氪001将于明年一季度上市 现款猎装同步推新配色

态度原创

本地
健康
家居
公开课
军事航空

本地新闻

中秋逛白塔寺,体验国医妙荟雅集

刷酸祛痘,为什么有人翻车?

家居要闻

2026建博会(广州) 公装联探展交流活动

公开课

李玫瑾:为什么性格比能力更重要?

军事要闻

美防长宣布组建“自主作战司令部”

无障碍浏览 进入关怀版