《深度剖析CPython解釋器》33. 為什么 obj == obj 為 False、[obj] == [obj] 為 True


楔子

今天同事在用 pandas 做數據處理的時候,不小心被 nan 坑了一下,他當時被坑的原因類似下面:

import numpy as np

print(np.nan == np.nan)  # False
print([np.nan] == [np.nan])  # True

為了嚴謹,我們再舉個栗子:

class A:

    def __eq__(self, other):
        return False


a1 = A()
a2 = A()

print(a1 == a1, a2 == a2)  # False False
print([a1, a2] == [a1, a2])  # True

為什么會出現這個結果呢?我們知道兩個列表(元組也是同理)如果相等,那么首先列表里面的元素個數要相同、並且相同索引對應的元素也要相等。但問題是這里的 a1 不等於 a1、a2 也不等於 a2,那為啥 [a1, a2] 和 [a1, a2] 就相等了呢?

其實原因很好想,那就是 Python 解釋器在比較兩個列表中的元素的時候,會先比較它們的引用的對象的地址是否相等,也就是看它們是否引用了同一個對象,如果是同一個對象,那么直接得到 True,然后比較下一個,如果不是同一個對象,那么再比較對應的值是否相同。所以這里 a1 == a1 明明返回 False,但是放在列表中就變成了 True,原因就在於它們引用的是同一個對象。

那么下面就來從解釋器源代碼的角度來驗證這一結論(版本為 3.9.0),其實后續涉及到的內容在之前就已經說過了,只不過因為比較簡單就一筆帶過了,所以這次就針對這個例子專門分析一下。

Python 的列表之間是如何比較的

要想知道底層是如何比較的,那么最好的辦法就是先看一下字節碼。

import dis

code = "[] == []"
dis.dis(compile(code, "<file>", "exec"))
"""
  1           0 BUILD_LIST               0
              2 BUILD_LIST               0
              4 COMPARE_OP               2 (==)
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
"""

第一列:表示源代碼的行號,我們這里只有一行代碼。

第二列:表示指令的偏移量,每一條指令都占兩個字節,第一個字節存放指令序列本身,第二個字節存放指令所需要的參數。所以指令從上到下的偏移量是 0 2 4 6 8 ......。

第三列:表示指令序列,在 C 中就是一個宏,會被替換為一個整數。Python 底層總共定義 120 多個指令序列,可以在 Include/opcode.h 頭文件中查看。

第四列:表示指令參數。

所以開頭的兩個 BUILD_LIST 表示構建列表,后面的指令參數表示元素個數,因為是空列表,所以為 0。兩個列表構建完畢顯然就要進行比較了,因此指令序列是 COMPARE_OP,而后面的指令參數是 2,代表啥含義呢?

COMPARE_OP 表示比較,但是比較也分為:小於、小於等於、等於、不等於、大於、大於等於,那么到底是哪一種呢?顯然要通過指零參數給出,而這里指定的是等於,所以指令參數是 2。至於指令參數后面的 (==) 則是 dis 模塊幫你添加的,告訴你該指令參數的含義,方便理解。

因此我們的關注點就在 COMPARE_OP 這條指令序列對應的實現當中,而 Python 底層的指令序列對應的實現都位於 Python/ceval.c 中,在里面有一個 _PyEval_EvalFrameDefault 函數,以棧幀(PyFrameObject)為單位。該函數里面有一個無限的 for 循環,會不斷地循環取出字節碼中每一條指令序列和指令參數進行執行,直到將該棧幀內部的字節碼全部執行完畢,然后退出循環。因此執行邏輯也是在這個 for 循環里面的,沒錯,for 循環里面有一個巨型的 switch,每一個指令序列都對應一個 case 語句,所以這個 switch 里面有 120 多個 case 語句,然后不同的指令序列走不同的 case,因此 _PyEval_EvalFrameDefault 這個函數非常長,總共多達 3000 行。

那么下面我們就來看看 COMPARE_OP 對應的指令實現,不過這里多提一句:不光是列表,其它對象進行比較的時候對應的指令序列也是 COMPARE_OP。

我們來分析一下:

case TARGET(COMPARE_OP): {
    // 這里的 oparg 表示的就是指令參數,顯然它和指令序列(opcode)在進入 switch 語句之前就已經被獲取
    // 然后這里斷言 oparg 必須要小於等於 Py_GE,因為比較操作符中最大的就是 Py_GE,而我們這里是 ==,所以 oparg 的值等於 2
    assert(oparg <= Py_GE);
    // BUILD_LIST 構建的兩個列表(指針)會被壓入運行時棧,然后這里再將其獲取
    // 當然這里只是以列表為例,但我們說進行比較的不一定是列表,可以是任意對象
    // 因此 right 就是比較操作符(我們這里是 ==)右邊的變量,left 就是左邊的變量
    PyObject *right = POP();  // 元素會從棧中彈出
    PyObject *left = TOP();   // 注意這里是 TOP(),不是 POP(),所以操作符左邊的變量還留在棧里面
    // 調用 PyObject_RichCompare,傳入 left、right、oparg 進行調用,得到返回結果 res
    // 顯然具體的比較邏輯就在 PyObject_RichCompare 里面
    PyObject *res = PyObject_RichCompare(left, right, oparg);
    // 用 res 將棧頂的元素替換掉,所以操作符左邊的變量不需要從棧里面彈出,直接將結果與之替換即可
    // 最后再返回
    SET_TOP(res);
    Py_DECREF(left);
    Py_DECREF(right);
    if (res == NULL)
        goto error;
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH();
}

這里涉及到了運行時棧,具體細節就不再贅述了,總之運行時棧是必不可少的,因為 Python 的指令只能有一個指令參數,但是 PyObject_RichCompare 函數需要三個參數,因此其它的參數只能通過運行時棧給出。

我們這里只需要知道,Python 中的比較,在底層會調用 PyObject_RichCompare 函數即可:

a == b  # PyObject_RichCompare(a, b, Py_EQ)
a >= b  # PyObject_RichCompare(a, b, Py_GE)
a != b  # PyObject_RichCompare(a, b, Py_NE)
...

下面來看看 PyObject_RichCompare 里面的邏輯,該函數藏身於 Objects/object.c 中。

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{   // 參數 v 就是上面的 left、w 就是 right、op 就是 oparg
    
    // 獲取線程狀態對象,這里不需要關注
    PyThreadState *tstate = _PyThreadState_GET();
    // 0 <= op <= 5
    assert(Py_LT <= op && op <= Py_GE);
    // 如果有一方為 NULL,則調用失敗
    if (v == NULL || w == NULL) {
        if (!_PyErr_Occurred(tstate)) {
            PyErr_BadInternalCall();
        }
        return NULL;
    }
    // 這里的 _Py_EnterRecursiveCall 和結尾的 _Py_LeaveRecursiveCall 會成對出現,主要是用於遞歸比較的,舉個栗子
    /*
        a = [None]
        a.append(a)
        print(a)  # [None, [...]]
        print(a[1][1][1][1][1][1][1][1][1][1])  # [None, [...]]
        print(a[1][1][1][1][1][1][1][1][1][0])  # None
        print(a == a)  # True
    */
    // 顯然 a 后面無論接多少個 [1] 都是合法的,因此就意味着要無限地比較下去,而 Python 顯然不會允許這種情況發生
    // 因此這一步就是為了應對這種情況出現
    if (_Py_EnterRecursiveCall(tstate, " in comparison")) {
        return NULL;
    }
    // 調用 do_richcompare,得到返回結果
    PyObject *res = do_richcompare(tstate, v, w, op);
    _Py_LeaveRecursiveCall(tstate);
    return res;
}

可以看到 PyObject_RichCompare 里面也不是真正負責執行比較邏輯的,該函數相當於做了一些檢測,而比較的結果是調用 do_richcompare 得到的,顯然我們需要到這個函數中查看,該函數同樣位於 Objects/object.c 中。

static PyObject *
do_richcompare(PyThreadState *tstate, PyObject *v, PyObject *w, int op)
{   
    // richcmpfunc f 相當於聲明一個比較函數,因為 Python 將每個比較操作都抽象成了一個魔法方法,比如:__ge__、__eq__ 等等
    // 雖然在 Python 中不同的比較操作對應不同的魔法方法,但底層對應的都是 PyTypeObject 的 tp_richcompare 成員
    // 該成員負責所有的比較操作,至於到底是哪一種,則由參數來控制
    /* 因此我們看到具體的比較邏輯,還是定義在對應的類對象中
       比如:
           list 對象的比較邏輯定義在 PyList_Type -> tp_richcompare 中
           tuple 對象的比較邏輯定義在 PyTuple_Type -> tp_richcompare 中
           Dict 對象的比較邏輯定義在 PyDict_Type -> tp_richcompare 中
           Set 對象的比較邏輯定義在 PySet_Type -> tp_richcompare 中
    */
    richcmpfunc f;
    // 用於存儲比較之后的結果
    PyObject *res;
    int checked_reverse_op = 0;
    
    /* Py_TYPE(obj) 表示獲取 obj 的類型;
       Py_IS_TYPE(obj, cls) 則是判斷 obj 的類型是否為 cls
       PyType_IsSubtype(cls1, cls2) 負責判斷 cls1 是否是 cls2 的子類
       所以下面 if 語句的含義就是:當 v 和 w 的類型不同、並且 w 的類型是 v 的類型的子類、
       並且 w 的類型對象內部的 tp_richcompare 成員不為 NULL,然后走這個分支。
       
       直接說的話,可能不是很好解釋這個 if 語句到底在做什么,我們可以用一個 Python 測試用例解釋一下:
       class A:
           def __eq__(self, other):
               return "A"
       class B(A):
           def __eq__(self, other):
               return "B"
       print(A() == B())  # B
       
       我們知道默認情況下,如果操作符左邊的兩個對象之間沒有任何關系,那么比較的時候優先會找操作符左邊的對象的魔法方法
       所以如果 B 不繼承 A,也就是 A 和 B 自己沒有任何關系,那么按照優先級,A() == B() 就會返回字符串 "A"
       但如果操作符 "右側的對象的類對象" 是 "左側的對象的類對象" 的子類,那么這個規則就會被打破
       解釋器就會執行操作符右側的對象的魔法方法,所以這里 B 繼承 A,A() == B() 返回了字符串 "B"
       這個 if 語句就是來干這件事的,因此這里的 f 等於 Py_TYPE(w)->tp_richcompare
       
    */
    if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
        PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
        (f = Py_TYPE(w)->tp_richcompare) != NULL) {
        checked_reverse_op = 1;
        // 會取出 w 對應的 tp_richcompare,將參數傳遞進去,進行調用
        // 其中 _Py_SwappedOp[op] 是負責將 op 以宏的形式傳遞,比如 op 是 2,那么 _Py_SwappedOp[op] 就是 Py_EQ,不過結果也是 2
        // 調用之后將結果保存起來
        res = (*f)(w, v, _Py_SwappedOp[op]);
        /* 然后這里對 res 有一個判斷,它是做什么的呢?
        首先我們上面說了,==、!=、>=、<=、<、> 在 Python 中對應不同的魔法方法
        但在底層解釋器的角度而言,對應的都是類型對象的 tp_richcompare
        至於底層執行這個函數的時候,到底執行哪一個比較操作,則是由參數控制
        比如我們上面實現了 __eq__,意味着 tp_richcompare 不為 NULL,那么進行比較的時候毫無疑問肯定會走這個分支
        但如果我們執行的不是 A() == B(),而是 A() != B(),那么這里的 res 就返回 Py_NotImplemented
        因為 A 和 B 內部都沒有定義 __ne__,因此 tp_richcompare 內部也就不包含處理比較操作為 != 時的邏輯
        所以這個分支一定會走,但返回的 res 會等於 Py_NotImplemented
        */
        if (res != Py_NotImplemented)
            // 返回了 res,並且不等於 Py_NotImplemented,才會返回
            return res;
        Py_DECREF(res);
    }
    
    // 如果不是上面那種情況,那么就看 v 是否定義了相應的魔法方法,也就是 Py_TYPE(v) 的 tp_richcompare 成員是否不為 NULL
    // 如果有的話就取出,然后傳遞參數進行調用,其它邏輯類似
    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    // 如果 Py_TYPE(v) 的 tp_richcompare 成員為 NULL,或者 res 為 Py_NotImplemented
    // 就意味着在 Python 的層面,操作符左邊的對象內部沒有定義該操作符對應的魔法方法(可能定義了別的)
    // 那么此時會去看操作符右側的對象內部是否有相應的魔法方法,所以這里會看 Py_TYPE(w) 的 tp_richcompare 是否不為 NULL
    if (!checked_reverse_op && (f = Py_TYPE(w)->tp_richcompare) != NULL) {
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    
    /* 走到這里說明,要么上面的三個 if 分支一個都沒有通過,即操作兩邊的對象的內部都沒有定義任何關於比較操作的魔法方法
    或者通過了,但返回的 res 等於 Py_NotImplemented,也就是定義了比較操作的魔法方法,
    但是當前執行的操作符對應的魔法方法沒有實現 */
    // 不過對於 Python 而言,== 和 != 是永遠不會報錯的,所以還要檢測操作符是不是 == 或 !=
    switch (op) {
    /* 盡管指定的操作符沒有實現,但如果操作符是 == 或者 !=,也就是 op 為 Py_EQ 或者 Py_NE 時
    那么就比較兩個對象的內存地址,比如 class A: pass
    A 里面沒有實現任何的魔法方法,但 a = A(); a == a 就是 True,因為對象的內存地址是一樣的
    這里說明一下,Python 中的變量在 C 的層面就是一個泛型指針(PyObject *),它存儲的不是對象(PyObject),而是對象的地址
    變量在傳遞的時候會傳遞地址,但是在操作一個變量時會自動操作變量指向的內存
    所以在判斷兩個變量是否指向同一個對象的時候(相當於 is),在 C 的層面只需要比較兩個指針是否相等即可
    而在比較兩個變量指向的對象是否相等(也就是 == ),那么會將兩個變量指向的對象所維護的值取出來,調用 PyObject_RichCompare 進行比較
    */
    case Py_EQ:
        // 這里的 v 和 w 顯然就相當於 Python 中的變量,就是一個指針
        // 因此判斷兩個變量是否指向同一個對象,直接判斷這兩個指針存的地址是否相等即可
        res = (v == w) ? Py_True : Py_False;
        break;
    case Py_NE:
        // 同理
        res = (v != w) ? Py_True : Py_False;
        break;
    // 如果比較操作符不是 == 或者 !=,那么就不好意思了,這兩個實例之間不允許執行當前的比較操作
    default:
        _PyErr_Format(tstate, PyExc_TypeError,
                      "'%s' not supported between instances of '%.100s' and '%.100s'",
                      opstrings[op],
                      Py_TYPE(v)->tp_name,
                      Py_TYPE(w)->tp_name);
        return NULL;
    }
    // 增加引用計數,返回
    Py_INCREF(res);
    return res;
}

以上就是 do_richcompare 的邏輯,它里面干了哪些事情呢?我們說里面三個 if 語句,主要用於確定到底該執行誰的魔法方法,比如 A 和 B 的實例進行比較:

  • 1. 如果 A 和 B 是不同的類、並且 B 還是 A 的子類,那么 "A() 操作符 B()" 會優先去 B 中查找操作符對應的魔法方法
  • 2. 否則的話,會按照優先級,先找 A(操作符左邊)的魔法方法
  • 3. 如果左邊沒有,那么就最后再找右邊

如果成功執行則直接返回,否則的話再對操作符進行判定,如果是 == 或者 != ,那么就比較兩個對象是否是同一個對象。

雖然花了一定的筆墨解釋完了比較操作在底層的邏輯,但是我們上面的問題本質上依舊沒有得到解決,我們還是不知道列表是如何比較的。因為很明顯,比較的核心在於類型對象的 tp_richcompare 中,它返回的結果就是這里的 res,所以如果我們想知道列表是如何比較的,那么就去 PyList_Type 的 tp_richcompare 成員中查看即可。

而 PyList_Type 的 tp_richcompare 成員對應的是 list_richcompare 函數,我們來看一下,其藏身於 Objects/listobject.c 中。

static PyObject *
list_richcompare(PyObject *v, PyObject *w, int op)
{    
    PyListObject *vl, *wl;
    Py_ssize_t i;
    // v 和 w 一定是 PyListObject *
    if (!PyList_Check(v) || !PyList_Check(w))
        Py_RETURN_NOTIMPLEMENTED;
    // 將 PyObject * 轉成 PyListObject *
    vl = (PyListObject *)v;
    wl = (PyListObject *)w;
    
    // 快分支:如果兩個列表連長度都不相等,那么當比較操作符是 == 或 != 的時候可以直接出結果
    if (Py_SIZE(vl) != Py_SIZE(wl) && (op == Py_EQ || op == Py_NE)) {
        // op 是 ==,返回 False
        // op 是 !=,返回 True
        if (op == Py_EQ)
            Py_RETURN_FALSE;
        else
            Py_RETURN_TRUE;
    }

    // 當列表長度相等,或者操作不是 == 或者 !=,那么就需要將兩個列表中的元素進行逐個比較了
    for (i = 0; i < Py_SIZE(vl) && i < Py_SIZE(wl); i++) {
        PyObject *vitem = vl->ob_item[i];
        PyObject *witem = wl->ob_item[i];
        /*我們說 Python 中變量本質上是一個指針,當然不光是變量,列表、元組、字典等容器里面容納的也是指針
        如果 vitem == witem,說明這兩個列表存儲的是同一個對象的指針(在 Python 里面也可以說引用)
        所以直接就 continue 了,說明當前位置的兩個元素是相等的
        因此我們就解釋了在最開始的問題中,為什么 a1 != a1、np.nan != np.nan,但 [a1] == [a1] 和 [np.nan] == [np.nan] 卻都是成立的
        再比如 None > None 會報錯,但是 [None] > [None] 卻不會,原因就在於 None 是單例的,地址相同
        而地址相同,那么就不比了(不管這兩個對象能不能比),而是直接看下一個元素 */
        if (vitem == witem) {
            continue;
        }
        
        // 增加引用計數
        Py_INCREF(vitem);
        Py_INCREF(witem);
        /*當不是同一個對象時,那就比較對象維護的值是否相同,這里又出現了一個 PyObject_RichCompareBool
        它在底層會調用之前說的 PyObject_RichCompare,只不過在調用之前會先檢測對象的地址是否相同
        如果是同一個對象,並且操作符是 ==、!=,那么會直接根據對象的地址判斷
        如果不是同一個對象,或者操作符不是 == 或者 !=,再調用 PyObject_RichCompare 比較對象維護的值之間的關系,
        此外該函數返回的是整型,為真返回 1、為假返回 0,報錯了返回 -1 */
        int k = PyObject_RichCompareBool(vitem, witem, Py_EQ);
        Py_DECREF(vitem);
        Py_DECREF(witem);
        if (k < 0)
            return NULL;
        // 為假直接 break,否則繼續下一輪循環
        if (!k)
            break;
    }
    
    // 兩個列表如果長度不相等,那么不斷遍歷的話,肯定有一方先結束
    // 下面邏輯就是處理長度不相等的情況,比較簡單,可以自己看一下
    if (i >= Py_SIZE(vl) || i >= Py_SIZE(wl)) {
        /* No more items to compare -- compare sizes */
        Py_RETURN_RICHCOMPARE(Py_SIZE(vl), Py_SIZE(wl), op);
    }

    /* We have an item that differs -- shortcuts for EQ/NE */
    if (op == Py_EQ) {
        Py_RETURN_FALSE;
    }
    if (op == Py_NE) {
        Py_RETURN_TRUE;
    }

    /* Compare the final item again using the proper operator */
    return PyObject_RichCompare(vl->ob_item[i], wl->ob_item[i], op);
}

因此到這里我們才算真正解釋了最開始的問題,在調用 PyObject_RichCompare 進行比較的時候,a1 == a1 會走內部的 __eq__,而在里面返回的 False。而 [a1] == [a1] 會走列表的 __eq__,而里面在比較元素的時候會先比較地址是否一樣,如果一樣直接就過了,根本不會走 type(a1) 里面的 __eq__。

Python 中的 in 也是同理,我們知道 a in b 等價於 b.__contains__(a),邏輯就是不斷地對 b 進行迭代,將得到的元素依次和 a 進行比較,如果相等則直接返回 True;如果迭代結束時一直沒有找到和 a 相等的元素,那么返回 False。所以邏輯很簡單,但我想說的是,這里比較相等的邏輯也會先比較對象的地址是否相同,如果地址相同直接為 True,當地址不同時,才會比較值是否一致。而從底層來看的話,這里的比較會調用 PyObject_RichCompareBool,而我們知道在這個函數里面會先比較地址是否一樣,地址不一樣再比較維護的值是否一樣(調用對應的 __eq__)。

class A:

    def __eq__(self, other):
        return False


a = A()
# 底層調用 PyObject_RichCompare,然后調用 __eq__
print(a == a)  # False
# 底層會調用 PyObject_RichCompareBool,會先判斷兩者是不是同一個對象
print(a in (a,))  # True
print(a in [a])  # True

以上就是由 nan 引發的一些思考,當然還是比較簡單的,因為是一些之前說過的內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM