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

为什么在 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)0hash(1)1hash(-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)

我们在哪里能找到-1tp_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_hashlong_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-05-15 16:15:53
国宴“黄金位置”,雷军羡慕惨了:左手库克、右手马斯克,对面黄仁勋

国宴“黄金位置”,雷军羡慕惨了:左手库克、右手马斯克,对面黄仁勋

南财社V
2026-05-15 17:40:58
新四军有个高敬亭,八路也有个高司令被处理,萧克为何擅杀大将?

新四军有个高敬亭,八路也有个高司令被处理,萧克为何擅杀大将?

大运河时空
2026-05-14 17:55:03
懂王到北京,日媒全程直播,日网评论竟然画风正常了

懂王到北京,日媒全程直播,日网评论竟然画风正常了

这里是东京
2026-05-14 22:07:30
新冠后遗症对人体的最大影响,很多人深受其害,有些人还不自知

新冠后遗症对人体的最大影响,很多人深受其害,有些人还不自知

呼吸科大夫胡洋
2026-02-22 11:39:12
追觅造车主体公司股权被冻结,实控人为俞浩

追觅造车主体公司股权被冻结,实控人为俞浩

新浪财经
2026-05-15 18:08:40
广州暴雨,小孩连人带车被冲走,外卖小哥狂奔救人!最新消息

广州暴雨,小孩连人带车被冲走,外卖小哥狂奔救人!最新消息

南方都市报
2026-05-15 17:22:46
178万吨榴莲涌入中国!进口量暴涨294%,泰国市场份额跌至不到60%

178万吨榴莲涌入中国!进口量暴涨294%,泰国市场份额跌至不到60%

阿纂看事
2026-05-14 15:18:51
夏奇拉:这已经是我第四届世界杯,Waka Waka是最爱歌曲之一

夏奇拉:这已经是我第四届世界杯,Waka Waka是最爱歌曲之一

懂球帝
2026-05-15 17:50:13
直击!广州暴雨水浸街,外卖小哥水中捞人...

直击!广州暴雨水浸街,外卖小哥水中捞人...

广州楼市发布
2026-05-15 15:55:26
经济学人:为什么中国的出口还将继续狂飙?

经济学人:为什么中国的出口还将继续狂飙?

新浪财经
2026-05-15 15:35:41
安徽安凯汽车股份有限公司国内营销公司常务副总经理周旭接受纪律审查和监察调查

安徽安凯汽车股份有限公司国内营销公司常务副总经理周旭接受纪律审查和监察调查

界面新闻
2026-05-15 16:25:41
白左圣母被驱赶出家:还会说有一天我们也是难民吗

白左圣母被驱赶出家:还会说有一天我们也是难民吗

番外行
2026-05-15 09:08:27
华尔街精英利用马斯克做局,使中国富豪损失百亿,还不敢对外公布

华尔街精英利用马斯克做局,使中国富豪损失百亿,还不敢对外公布

吴学华看天下
2024-11-12 19:42:48
中国气象局将重大气象灾害应急响应提升为Ⅲ级

中国气象局将重大气象灾害应急响应提升为Ⅲ级

财联社
2026-05-15 18:40:10
油价大降近5毛/升,今年“最大油价下跌”后,5月21日油价或再跌

油价大降近5毛/升,今年“最大油价下跌”后,5月21日油价或再跌

油价早知道
2026-05-15 09:49:34
牡丹花下死!送走马蓉又迎冯清,43岁王宝强,终究栽进“女人坑”

牡丹花下死!送走马蓉又迎冯清,43岁王宝强,终究栽进“女人坑”

小噎论事
2026-04-15 05:19:35
没有美国,泽连斯基为何依然能赢得战争?

没有美国,泽连斯基为何依然能赢得战争?

高博新视野
2026-05-14 07:30:18
美国歌手隆胸3年后反悔:太大了,准备取出

美国歌手隆胸3年后反悔:太大了,准备取出

影视情报室
2026-05-15 01:37:14
最美女星坏事干尽:三次入狱、鼓励丈夫肉体出轨、被摘5个器官

最美女星坏事干尽:三次入狱、鼓励丈夫肉体出轨、被摘5个器官

临云史策
2026-05-15 13:49:40
2026-05-15 21:23:00
Python猫 incentive-icons
Python猫
人生苦短,我用Python。博客:https://pythoncat.top
729文章数 8120关注度
往期回顾 全部

科技要闻

直降千元起步!苹果华为率先开启618让利

头条要闻

伊朗外长警告阿联酋 指责其直接参与对伊朗的军事行动

头条要闻

伊朗外长警告阿联酋 指责其直接参与对伊朗的军事行动

体育要闻

德约科维奇买的球队,从第6级联赛升入法甲

娱乐要闻

方媛为何要来《桃花坞6》没苦硬吃?

财经要闻

腾讯掉队,马化腾戳破真相

汽车要闻

高尔夫GTI刷新纽北纪录 ID. Polo GTI迎全球首秀

态度原创

房产
数码
本地
游戏
公开课

房产要闻

老黄埔热销之下,珠江春,为何去化仅3成?

数码要闻

OPPO Enco Air5标准版耳机5月20日首销

本地新闻

用苏绣的方式,打开江西婺源

LCK第二赛段:小局26连败,BRO横扫DNS

公开课

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

无障碍浏览 进入关怀版