△△請(qǐng)給“Python貓”加星標(biāo) ,以免錯(cuò)過(guò)文章推送

打開(kāi)網(wǎng)易新聞 查看精彩圖片

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

作者:Omair Majid

譯者:豌豆花下貓&Claude-3.5-Sonnet

時(shí)間:原文發(fā)布于 2021.07.16,翻譯于 2025.01.11

當(dāng)我在等待代碼編譯[1]的時(shí)候,我在 Reddit 的 r/Python 上看到了這個(gè)問(wèn)題:

hash(-1) == hash(-2) 是個(gè)彩蛋嗎?[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

是的,確實(shí)如此。真讓人驚訝!

讓我們看看其它一些常見(jiàn)的哈希值:

>>> hash(1)
1
>>> hash(0)
0
>>> hash(3)
3
>>> hash(-4)
-4

看起來(lái)所有小整數(shù)的哈希值都等于它們自身,除了-1...

現(xiàn)在我完全被這個(gè)問(wèn)題吸引住了。我試圖自己找出答案。在接下來(lái)的內(nèi)容中,我將帶你了解如何自己尋找這個(gè)答案。

如何開(kāi)始呢?什么能給我們一個(gè)權(quán)威的答案?

讓我們看看源代碼!Python 的實(shí)際實(shí)現(xiàn)代碼!

獲取源代碼

我假設(shè)你和我一樣,對(duì) Python 的源代碼在哪里完全沒(méi)有概念。

那么,我們(假設(shè)從未看過(guò) Python 的源代碼)如何獲取源代碼來(lái)回答最初的問(wèn)題呢?

也許我們可以用 Google?搜索 "python implementation" 會(huì)帶來(lái)一些有趣的結(jié)果。

我搜索的第一個(gè)結(jié)果[3]提到了 "CPython 參考實(shí)現(xiàn)"。

Github 上Python 組織[4]的第二個(gè)倉(cāng)庫(kù)就是 "cpython"。這看起來(lái)很靠譜。我們?nèi)绾未_保萬(wàn)無(wú)一失呢?

我們可以訪問(wèn) python.org。讓我們?nèi)サ皆创a下載頁(yè)面。最終我找到了Python 3.9.6 的壓縮包[5]。解壓后,README.rst也指向了 Github 上的 CPython。

好的,這就是我們的起點(diǎn)。讓我們獲取這些代碼,以便后續(xù)搜索:

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

--depth 1參數(shù)使git只獲取有限的歷史記錄。這樣可以讓克隆操作快很多。如果之后需要完整的歷史記錄,我們可以再獲取。

讓我們深入研究

在研究代碼時(shí),我們需要找到一個(gè)起點(diǎn)。最好是容易搜索的東西,比如一個(gè)簡(jiǎn)單的字符串,不會(huì)有太多誤導(dǎo)性的匹配。

也許我們可以使用hash函數(shù)的文檔?我們可以用help(hash)來(lái)查看文檔內(nèi)容:

>>> 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.

現(xiàn)在,我們可以用它來(lái)找到hash()的實(shí)現(xiàn):

$ 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 實(shí)現(xiàn)有關(guān),所以我們可以忽略它。functions.rst是一個(gè)文檔文件,所以也可以忽略。

Python/bltinmodule.c看起來(lái)很有趣。如果我們查看這個(gè)文件,會(huì)找到這樣一段代碼:

/*
...
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帶我來(lái)到這里[6]。看起來(lái)PyLongObject是 Python 整數(shù)的原生表示(這在稍后會(huì)派上用場(chǎng))。在瀏覽了PyLongObject文檔并重讀這段代碼后,看起來(lái)是這樣的:

  1. 我們調(diào)用PyObject_Hash來(lái)獲得一個(gè)對(duì)象的哈希值

  2. 如果計(jì)算出的哈希值是 -1,那表示是一個(gè)錯(cuò)誤

  • 看起來(lái)我們用 -1 來(lái)表示錯(cuò)誤,所以沒(méi)有哈希函數(shù)會(huì)為真實(shí)對(duì)象計(jì)算出 -1

我們將Py_Ssize_t轉(zhuǎn)換為PyLongObject(文檔中稱之為:"這是 PyObject 的子類型,表示一個(gè) Python 整數(shù)對(duì)象")

啊哈!這就解釋了為什么hash(0)0hash(1)1,hash(-2)-2,但hash(-1)不是-1。這是因?yàn)?code>-1在內(nèi)部被用來(lái)表示錯(cuò)誤。

但為什么hash(-1)-2呢?是什么將它設(shè)置成了這個(gè)值?

讓我們看看能否找出原因。

我們可以先查找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));
...

雖然有很多干擾,但唯一的實(shí)現(xiàn)似乎在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 繼承的類型,應(yīng)該無(wú)需顯式調(diào)用 PyType_Ready 就能工作,
     * 我們?cè)谶@里隱式調(diào)用 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); }

這段代碼相當(dāng)令人困惑。幸運(yùn)的是,注釋很清晰。在多次閱讀后,似乎這段代碼——考慮到類型的一些延遲加載(?)——先找到對(duì)象的類型(使用Py_TYPE)。然后尋找該類型的tp_hash函數(shù),并在 v 上調(diào)用該函數(shù):(*tp->tp_hash)(v)

我們?cè)谀睦锬苷业?code>-1的tp_hash呢?讓我們?cè)俅嗡阉?code>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
...

這是一個(gè)很長(zhǎng)的列表?;叵胍幌挛臋n中關(guān)于PyLongObject的說(shuō)明("這個(gè)...表示一個(gè) Python 整數(shù)對(duì)象"),我先查看下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類型對(duì)象的tp_hashlong_hash。讓我們看看這個(gè)函數(shù)。

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;
}

注意我刪除了大部分實(shí)現(xiàn)細(xì)節(jié)。但這個(gè)函數(shù)的結(jié)尾正好符合我們的預(yù)期:-1被保留用作錯(cuò)誤信號(hào),所以代碼明確地將該返回值轉(zhuǎn)換為-2

這就解釋了為什么hash(-1)最終與hash(-2)相同。這不是一個(gè)彩蛋,只是為了避免使用-1作為hash()方法的返回值,因此采取的變通方法。

這是正確/完整的答案嗎?

如前所述,我從未看過(guò) Python 代碼庫(kù)。我認(rèn)為自己找到了答案。但這是對(duì)的嗎?我可能完全錯(cuò)了。

幸運(yùn)的是,/u/ExoticMandibles 在 Reddit 帖子中提供了答案[7]

Python 的參考實(shí)現(xiàn)是 "CPython",這很可能就是你正在使用的 Python。CPython 是用 C 語(yǔ)言編寫的,與 Python 不同,C 語(yǔ)言沒(méi)有異常處理。所以,在 C 語(yǔ)言中,當(dāng)你設(shè)計(jì)一個(gè)函數(shù),并且想要表示"發(fā)生了錯(cuò)誤"時(shí),必須通過(guò)返回值來(lái)表示這個(gè)錯(cuò)誤。 CPython 中的 hash() 函數(shù)可能返回錯(cuò)誤,所以它定義返回值 -1 表示"發(fā)生了錯(cuò)誤"。但如果哈希計(jì)算正確,而對(duì)象的實(shí)際哈希值恰好是 -1,這可能會(huì)造成混淆。所以約定是:如果哈希計(jì)算成功,并得到值是 -1,就返回 -2。 在 CPython 中,整數(shù)("長(zhǎng)整型對(duì)象")的哈希函數(shù)中有專門的代碼來(lái)處理這種情況: https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967[8]

這正是我通過(guò)閱讀代碼推測(cè)出的結(jié)果。

結(jié)論

我從一個(gè)看似難以回答的問(wèn)題開(kāi)始。但是通過(guò)幾分鐘的代碼探索——Python 整潔的代碼庫(kù)使得查看它的代碼比我見(jiàn)過(guò)的其它代碼庫(kù)要容易得多——很容易就發(fā)現(xiàn)和理解了答案!如果你接觸過(guò)計(jì)算機(jī),這應(yīng)該不足為奇。這里沒(méi)有魔法,只有層層的抽象和代碼。

如果本文有什么啟示的話,那就是:查看源代碼![9](文檔可能會(huì)過(guò)時(shí),注釋可能不準(zhǔn)確,但源碼是永恒的。)

參考資料

等待代碼編譯: https://xkcd.com/303/

hash(-1) == hash(-2) 是個(gè)彩蛋嗎?: https://www.reddit.com/r/Python/comments/oks5km/is_hash_1hash2_an_easter_egg/

第一個(gè)結(jié)果: 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

如果你正在尋找優(yōu)質(zhì)的Python文章和項(xiàng)目,我必須向你推薦Python潮流周刊!

它精選全網(wǎng)的優(yōu)秀文章、教程、開(kāi)源項(xiàng)目、軟件工具、播客、視頻、熱門話題等豐富內(nèi)容,讓你緊跟技術(shù)最前沿,獲取最新的第一手學(xué)習(xí)資料!

歡迎點(diǎn)擊下方圖片,了解這份全世界知識(shí)密度最高、知識(shí)廣度最大的 Python 技術(shù)周刊。