33-为什么 obj == obj 为 False、[obj] == [obj] 为 True 楔子 今天同事在用 pandas 做数据处理的时候,不小心被 nan 坑了一下,他当时被坑的原因类似下面:
1 2 3 4 import numpy as npprint (np.nan == np.nan) print ([np.nan] == [np.nan])
为了严谨,我们再举个栗子:
1 2 3 4 5 6 7 8 9 10 11 class A : def __eq__ (self, other ): return False a1 = A() a2 = A() print (a1 == a1, a2 == a2) print ([a1, a2] == [a1, a2])
为什么会出现这个结果呢?我们知道两个列表(元组也是同理)如果相等,那么首先列表里面的元素个数要相同、并且相同索引对应的元素也要相等。但问题是这里的 a1 不等于 a1、a2 也不等于 a2,那为啥 [a1, a2] 和 [a1, a2] 就相等了呢?
其实原因很好想,那就是 Python 解释器在比较两个列表中的元素的时候,会先比较它们的引用的对象的地址是否相等,也就是看它们是否引用了同一个对象,如果是同一个对象,那么直接得到 True,然后比较下一个,如果不是同一个对象,那么再比较对应的值是否相同。所以这里 a1 == a1 明明返回 False,但是放在列表中就变成了 True,原因就在于它们引用的是同一个对象。
那么下面就来从解释器源代码的角度来验证这一结论(版本为 3.9.0),其实后续涉及到的内容在之前就已经说过了,只不过因为比较简单就一笔带过了,所以这次就针对这个例子专门分析一下。
Python 的列表之间是如何比较的 要想知道底层是如何比较的,那么最好的办法就是先看一下字节码。
1 2 3 4 5 6 7 8 9 10 11 12 import discode = "[] == []" 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。
我们来分析一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 case TARGET (COMPARE_OP) : { assert(oparg <= Py_GE); PyObject *right = POP(); PyObject *left = TOP(); PyObject *res = PyObject_RichCompare(left, right, oparg); 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 函数即可:
1 2 3 4 a == b a >= b a != b ...
下面来看看 PyObject_RichCompare 里面的逻辑,该函数藏身于 *Objects/object.c* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 PyObject * PyObject_RichCompare (PyObject *v, PyObject *w, int op) { PyThreadState *tstate = _PyThreadState_GET(); assert(Py_LT <= op && op <= Py_GE); if (v == NULL || w == NULL ) { if (!_PyErr_Occurred(tstate)) { PyErr_BadInternalCall(); } return NULL ; } if (_Py_EnterRecursiveCall(tstate, " in comparison" )) { return NULL ; } PyObject *res = do_richcompare(tstate, v, w, op); _Py_LeaveRecursiveCall(tstate); return res; }
可以看到 PyObject_RichCompare 里面也不是真正负责执行比较逻辑的,该函数相当于做了一些检测,而比较的结果是调用 do_richcompare 得到的,显然我们需要到这个函数中查看,该函数同样位于 *Objects/object.c* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 static PyObject *do_richcompare (PyThreadState *tstate, PyObject *v, PyObject *w, int op) { richcmpfunc f; PyObject *res; int checked_reverse_op = 0 ; 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 ; res = (*f)(w, v, _Py_SwappedOp[op]); if (res != Py_NotImplemented) return res; Py_DECREF(res); } if ((f = Py_TYPE(v)->tp_richcompare) != NULL ) { res = (*f)(v, w, op); if (res != Py_NotImplemented) return res; Py_DECREF(res); } 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); } switch (op) { case Py_EQ: 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* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 static PyObject *list_richcompare (PyObject *v, PyObject *w, int op) { PyListObject *vl, *wl; Py_ssize_t i; if (!PyList_Check(v) || !PyList_Check(w)) Py_RETURN_NOTIMPLEMENTED; vl = (PyListObject *)v; wl = (PyListObject *)w; if (Py_SIZE(vl) != Py_SIZE(wl) && (op == Py_EQ || op == Py_NE)) { 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]; if (vitem == witem) { continue ; } Py_INCREF(vitem); Py_INCREF(witem); int k = PyObject_RichCompareBool(vitem, witem, Py_EQ); Py_DECREF(vitem); Py_DECREF(witem); if (k < 0 ) return NULL ; if (!k) break ; } if (i >= Py_SIZE(vl) || i >= Py_SIZE(wl)) { Py_RETURN_RICHCOMPARE(Py_SIZE(vl), Py_SIZE(wl), op); } if (op == Py_EQ) { Py_RETURN_FALSE; } if (op == Py_NE) { Py_RETURN_TRUE; } 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__)。
1 2 3 4 5 6 7 8 9 10 11 12 class A : def __eq__ (self, other ): return False a = A() print (a == a) print (a in (a,)) print (a in [a])
以上就是由 nan 引发的一些思考,当然还是比较简单的,因为是一些之前说过的内容。