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

为什么在 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.

相关推荐
热点推荐
高下立判!《马卡报》对比梅西和C罗的国家队结局!

高下立判!《马卡报》对比梅西和C罗的国家队结局!

历史第一人梅西
2026-10-01 22:26:08
名古屋大翻车!中国客场151金碾压,日本差百金,印度争三成笑话

名古屋大翻车!中国客场151金碾压,日本差百金,印度争三成笑话

林子说事
2026-10-01 18:11:45
因为是张维伊才结婚!董璇聊再婚,认可老公适合结婚过日子

因为是张维伊才结婚!董璇聊再婚,认可老公适合结婚过日子

布丁冰淇淋
2026-10-01 19:21:48
30岁阿根廷高官炮轰中国,马岛问题上站队英国,访华前先捅娄子?

30岁阿根廷高官炮轰中国,马岛问题上站队英国,访华前先捅娄子?

阿纂看事
2026-09-10 14:57:12
我在ICU当了15年护士,发现一个规律:平时对父母不孝顺的子女,在签放弃治疗同意书时,反而哭得最狠最伤心

我在ICU当了15年护士,发现一个规律:平时对父母不孝顺的子女,在签放弃治疗同意书时,反而哭得最狠最伤心

世间烟火集
2026-08-22 16:54:16
凌晨发觉客厅声控灯突然亮了,我立马躲进床底,刚想拿出手机去求救

凌晨发觉客厅声控灯突然亮了,我立马躲进床底,刚想拿出手机去求救

悬案解密档案
2025-11-28 10:16:22
震惊!网传某半导体大厂号召全员降薪共渡难关,一同降薪20%,财报曝光:老总年薪大涨33%,年薪反倒从600万涨至800万

震惊!网传某半导体大厂号召全员降薪共渡难关,一同降薪20%,财报曝光:老总年薪大涨33%,年薪反倒从600万涨至800万

火山詩话
2026-09-27 13:25:11
不用怀疑,小米今年55万辆的目标,不可能完成了!

不用怀疑,小米今年55万辆的目标,不可能完成了!

互联网.乱侃秀
2026-10-01 12:40:30
破案了!林诗栋夺冠领奖全程黑脸,终于知道他为什么笑不出来

破案了!林诗栋夺冠领奖全程黑脸,终于知道他为什么笑不出来

观察者海风
2026-09-30 11:42:54
教育部任命!211文科大学,迎新副校长

教育部任命!211文科大学,迎新副校长

双一流高校
2026-10-02 00:25:41
心理学中,有个永不抑郁的秘诀

心理学中,有个永不抑郁的秘诀

鼠鼠健康图鉴
2026-09-30 06:00:13
不再称高市早苗为“首相”!中方这个表述值得关注

不再称高市早苗为“首相”!中方这个表述值得关注

看看新闻Knews
2026-10-01 01:55:12
拳王泰森来瞻仰毛主席遗容,他出来说了一句这辈子都没敢说的话

拳王泰森来瞻仰毛主席遗容,他出来说了一句这辈子都没敢说的话

游文刀
2026-09-30 00:14:51
女性长期使用情趣用品,身体可能出现哪些不适?不少人缺乏认知

女性长期使用情趣用品,身体可能出现哪些不适?不少人缺乏认知

看世界的人
2026-08-07 09:22:50
日本最奇葩的将军?带14万日军却不打仗,投降时战俘营已自己盖好

日本最奇葩的将军?带14万日军却不打仗,投降时战俘营已自己盖好

千秋文化
2026-09-29 21:26:19
梁洛施和男友马浴柯出席路演,十指紧扣大方秀恩爱,尽显甜蜜

梁洛施和男友马浴柯出席路演,十指紧扣大方秀恩爱,尽显甜蜜

阿废冷眼观察所
2026-10-02 00:30:52
北大化学才女李天乐,利用职务之便,将清华毕业的丈夫,一点点“熬”死在病房!她领出的剧毒为何消失了?

北大化学才女李天乐,利用职务之便,将清华毕业的丈夫,一点点“熬”死在病房!她领出的剧毒为何消失了?

史源观点
2026-09-29 19:09:22
杨毅:杨鸣出来没想到这么受欢迎 他肯定还有执教意愿等最合适机会

杨毅:杨鸣出来没想到这么受欢迎 他肯定还有执教意愿等最合适机会

狼叔评论
2026-10-01 18:49:04
七天前C罗曾表示:如果教练不打算用我,我今天就可以离开

七天前C罗曾表示:如果教练不打算用我,我今天就可以离开

懂球帝
2026-10-01 08:54:09
连续嘲讽郭德纲两月,杨议万万没料到,对方人气丝毫未减

连续嘲讽郭德纲两月,杨议万万没料到,对方人气丝毫未减

草莓解说体育
2026-10-02 01:21:35
2026-10-02 02:16:49
Python猫 incentive-icons
Python猫
人生苦短,我用Python。博客:https://pythoncat.top
758文章数 8122关注度
往期回顾 全部

科技要闻

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

头条要闻

桂林市文促会深夜发布致歉信:向阿丘诚恳致歉

头条要闻

桂林市文促会深夜发布致歉信:向阿丘诚恳致歉

体育要闻

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

娱乐要闻

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

财经要闻

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

汽车要闻

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

态度原创

本地
时尚
家居
数码
公开课

本地新闻

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

在米兰,用时装唤醒内心的自我

家居要闻

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

数码要闻

亚马逊发布全新Kindle:取消传统边框 正面屏幕实现全面平整化

公开课

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

无障碍浏览 进入关怀版