成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

剖析字節(jié)碼指令,以及 Python 賦值語句的原理

開發(fā) 前端
雖然在 Python 里面用于比較的魔法方法有多個(gè),比如 __eq__、__le__、__gt__ 等等。但在底層,它們都對應(yīng) tp_richcompare,至于具體是哪一種,則由參數(shù)控制。所以我們實(shí)現(xiàn)任意一個(gè)用于比較的魔法方法,底層都會(huì)實(shí)現(xiàn) tp_richcompare。

楔子

前面我們考察了虛擬機(jī)執(zhí)行字節(jié)碼指令的原理,那么本篇文章就來看看這些指令對應(yīng)的邏輯是怎樣的,每個(gè)指令都做了哪些事情。當(dāng)然啦,由于字節(jié)碼指令有兩百多個(gè),我們沒辦法逐一分析,這里會(huì)介紹一些常見的。至于其它的指令,會(huì)隨著學(xué)習(xí)的深入,慢慢揭曉。

介紹完常見指令之后,我們會(huì)探討 Python 賦值語句的背后原理,并分析它們的差異。

常用指令

有一部分指令出現(xiàn)的頻率極高,非常常用,我們來看一下。

我們舉例說明:

import dis

name = "古明地覺"

def foo():
    age = 16
    print(age)
    global name
    print(name)
    name = "古明地戀"

dis.dis(foo)
"""
  1           0 RESUME                   0

  2           2 LOAD_CONST               1 (16)
              4 STORE_FAST               0 (age)

  3           6 LOAD_GLOBAL              1 (NULL + print)
             16 LOAD_FAST                0 (age)
             18 CALL                     1
             26 POP_TOP

  5          28 LOAD_GLOBAL              1 (NULL + print)
             38 LOAD_GLOBAL              2 (name)
             48 CALL                     1
             56 POP_TOP

  6          58 LOAD_CONST               2 ('古明地戀')
             60 STORE_GLOBAL             1 (name)
             62 RETURN_CONST             0 (None)
"""

我們看到 age = 16 對應(yīng)兩條字節(jié)碼指令。

  • LOAD_CONST:加載一個(gè)常量,這里是 16;
  • STORE_FAST:在局部作用域中創(chuàng)建一個(gè)局部變量,這里是 age;

print(age) 對應(yīng)四條字節(jié)碼指令。

  • LOAD_GLOBAL:在局部作用域中加載一個(gè)全局變量或內(nèi)置變量,這里是 print;
  • LOAD_FAST:在局部作用域中加載一個(gè)局部變量,這里是 age;
  • CALL:函數(shù)調(diào)用;
  • POP_TOP:從棧頂彈出返回值;

print(name) 對應(yīng)兩條字節(jié)碼指令。

  • LOAD_GLOBAL:在局部作用域中加載一個(gè)全局變量或內(nèi)置變量,這里是 print;
  • LOAD_GLOBAL:在局部作用域中加載一個(gè)全局變量或內(nèi)置變量,這里是 name;
  • CALL:函數(shù)調(diào)用;
  • POP_TOP:從棧頂彈出返回值;

name = "古明地戀" 對應(yīng)兩條字節(jié)碼指令。

  • LOAD_CONST:加載一個(gè)常量,這里是 "古明地戀";
  • STORE_GLOBAL:在局部作用域中創(chuàng)建一個(gè) global 關(guān)鍵字聲明的全局變量,這里是 name;

這些指令非常常見,因?yàn)樗鼈兒统A俊⒆兞康募虞d,以及變量的定義密切相關(guān),你寫的任何代碼在反編譯之后都少不了它們的身影。

注:不管加載的是常量、還是變量,得到的永遠(yuǎn)是指向?qū)ο蟮闹羔槨?/p>

變量賦值的具體細(xì)節(jié)

這里再通過變量賦值感受一下字節(jié)碼的執(zhí)行過程,首先關(guān)于變量賦值,你平時(shí)是怎么做的呢?

圖片圖片

這些賦值語句背后的原理是什么呢?我們通過字節(jié)碼來逐一回答。

1)a, b = b, a 的背后原理是什么?

想要知道背后的原理,查看它的字節(jié)碼是我們最好的選擇。

0 RESUME                   0

     2 LOAD_NAME                0 (b)
     4 LOAD_NAME                1 (a)
     6 SWAP                     2
     8 STORE_NAME               1 (a)
    10 STORE_NAME               0 (b)
    12 RETURN_CONST             0 (None)

里面關(guān)鍵的就是 SWAP 指令,雖然我們還沒看這個(gè)指令,但也能猜出來它負(fù)責(zé)交換棧里面的兩個(gè)元素。假設(shè) a 和 b 的值分別為 22、33,看一下運(yùn)行時(shí)棧的變化過程。

圖片圖片

示意圖還是很好理解的,關(guān)鍵就在于 SWAP 指令,它是怎么交換元素的呢?

TARGET(SWAP) {
    // 獲取棧頂元素
    PyObject *top = stack_pointer[-1];
    // oparg 表示交換的元素個(gè)數(shù)
    // 所以 stack_pointer[-oparg] 表示獲取棧底元素
    PyObject *bottom = stack_pointer[-(2 + (oparg-2))];
    #line 3389 "Python/bytecodes.c"
    assert(oparg >= 2);
    #line 4680 "Python/generated_cases.c.h"
    // 將棧頂元素和棧頂元素進(jìn)行交換
    stack_pointer[-1] = bottom;
    stack_pointer[-(2 + (oparg-2))] = top;
    DISPATCH();
}

執(zhí)行 SWAP 指令之前,棧里有兩個(gè)元素,棧頂元素是 a,棧底元素是 b。執(zhí)行 SWAP 指令之后,棧頂元素是 b,棧底元素是 a。然后后面的兩個(gè) STORE_NAME 會(huì)將棧里面的元素 b、a 依次彈出,賦值給 a、b,從而完成變量交換。

2)a, b, c = c, b, a 的背后原理是什么?

老規(guī)矩,還是查看字節(jié)碼,因?yàn)橐磺姓嫦喽茧[藏在字節(jié)碼當(dāng)中。

0 RESUME                   0

     2 LOAD_NAME                0 (c)
     4 LOAD_NAME                1 (b)
     6 LOAD_NAME                2 (a)
     8 SWAP                     3
    10 STORE_NAME               2 (a)
    12 STORE_NAME               1 (b)
    14 STORE_NAME               0 (c)
    16 RETURN_CONST             0 (None)

整個(gè)過程和 a, b = b, a 是相似的,首先按照從左往右的順序,將等號右邊的變量依次壓入棧中,然后調(diào)用 SWAP 指令交換棧頂和棧底的元素。最后將棧里的元素彈出,按照從左往右的順序,依次賦值給等號左邊的變量。

所以 SWAP 適用于兩個(gè)或三個(gè)變量之間的交換,兩個(gè)變量交換很好理解,關(guān)鍵是三個(gè)變量交換,依舊只需要一個(gè) SWAP 指令,因?yàn)橹虚g的元素是不需要?jiǎng)拥摹?/p>

3)a, b, c, d = d, c, b, a 的背后原理是什么?它和上面提到的 1)和 2)有什么區(qū)別呢?

我們還是看一下字節(jié)碼。

0 RESUME                   0

     2 LOAD_NAME                0 (d)
     4 LOAD_NAME                1 (c)
     6 LOAD_NAME                2 (b)
     8 LOAD_NAME                3 (a)
    10 BUILD_TUPLE              4
    12 UNPACK_SEQUENCE          4
    16 STORE_NAME               3 (a)
    18 STORE_NAME               2 (b)
    20 STORE_NAME               1 (c)
    22 STORE_NAME               0 (d)
    24 RETURN_CONST             0 (None)

將等號右邊的變量,按照從左往右的順序,依次壓入棧中,但此時(shí)沒有直接將棧里面的元素做交換,而是構(gòu)建一個(gè)元組。因?yàn)橥鶙@锩鎵喝肓怂膫€(gè)元素,所以 BUILD_TUPLE 后面的 oparg 是 4,表示構(gòu)建長度為 4 的元組。

TARGET(BUILD_TUPLE) {
    // stack_pointer 指向運(yùn)行時(shí)棧的棧頂,oparg 表示運(yùn)行時(shí)棧的元素個(gè)數(shù)
    // 那么 stack_pointer - oparg 便指向運(yùn)行時(shí)棧的棧底
    PyObject **values = (stack_pointer - oparg);
    PyObject *tup;  // 指向創(chuàng)建的元組
    #line 1489 "Python/bytecodes.c"
    // 運(yùn)行時(shí)棧本質(zhì)上就是個(gè)數(shù)組,索引從小到大的方向表示棧底到棧頂?shù)姆较?    // 當(dāng)執(zhí)行 a, b, c, d = d, c, b, a 時(shí),會(huì)將右側(cè)的變量依次入棧
    // 運(yùn)行時(shí)棧里的元素從棧底到棧頂依次是 d、c、b、a
    // 拷貝數(shù)組(運(yùn)行時(shí)棧)里的元素,創(chuàng)建元組,結(jié)果是 (d, c, b, a)
    tup = _PyTuple_FromArraySteal(values, oparg);
    if (tup == NULL) { STACK_SHRINK(oparg); goto error; }
    #line 2038 "Python/generated_cases.c.h"
    // 清空運(yùn)行時(shí)棧
    STACK_SHRINK(oparg);
    // 然后將 tup 入棧
    STACK_GROW(1);
    stack_pointer[-1] = tup;
    DISPATCH();
}

// Object/tupleobject.c
PyObject *
_PyTuple_FromArraySteal(PyObject *const *src, Py_ssize_t n)
{
    if (n == 0) {
        return tuple_get_empty();
    }
    // 申請長度為 n 的元組
    PyTupleObject *tuple = tuple_alloc(n);
    // ...
    PyObject **dst = tuple->ob_item;
    // 從 0 開始,將數(shù)組里的元組依次拷貝到元組中
    for (Py_ssize_t i = 0; i < n; i++) {
        PyObject *item = src[i];
        dst[i] = item;
    }
    _PyObject_GC_TRACK(tuple);
    return (PyObject *)tuple;
}

此時(shí)棧里面只有一個(gè)元素,指向一個(gè)元組。接下來是 UNPACK_SEQUENCE,負(fù)責(zé)對序列進(jìn)行解包,它的指令參數(shù)也是 4,表示要解包的序列的長度為 4,我們來看看它的邏輯。

TARGET(UNPACK_SEQUENCE) {
    PREDICTED(UNPACK_SEQUENCE);
    // 獲取棧頂元素,也就是上一步創(chuàng)建的元組:(d, c, b, a)
    PyObject *seq = stack_pointer[-1];
    #line 1057 "Python/bytecodes.c"
    // ...
    // 將元組里的元素彈出,并依次入棧,此時(shí)方向和之前是相反的
    PyObject **top = stack_pointer + oparg - 1;
    int res = unpack_iterable(tstate, seq, oparg, -1, top);
    #line 1462 "Python/generated_cases.c.h"
    Py_DECREF(seq);
    #line 1070 "Python/bytecodes.c"
    if (res == 0) goto pop_1_error;
    #line 1466 "Python/generated_cases.c.h"
    STACK_SHRINK(1);
    STACK_GROW(oparg);
    next_instr += 1;
    DISPATCH();
}

假設(shè)變量 a b c d 的值分別為 1 2 3 4,我們畫圖來描述一下整個(gè)過程。

圖片圖片

可以看到當(dāng)交換的變量多了之后,不會(huì)直接在運(yùn)行時(shí)棧里面操作,而是將棧里面的元素挨個(gè)彈出、構(gòu)建元組(準(zhǔn)確的說應(yīng)該是先構(gòu)建元組,然后再清空運(yùn)行時(shí)棧)。接著再按照指定順序,將元組里面的元素重新壓到棧里面。

當(dāng)然不管是哪一種做法,Python 在進(jìn)行變量交換時(shí)所做的事情是不變的,核心分為三步。

  • 1)將等號右邊的變量,按照從左往右的順序,依次壓入棧中;
  • 2)對運(yùn)行時(shí)棧里面元素的順序進(jìn)行調(diào)整;
  • 3)將運(yùn)行時(shí)棧里面的元素挨個(gè)彈出,還是按照從左往右的順序,再依次賦值給等號左邊的變量;

只不過當(dāng)變量不多時(shí),調(diào)整元素位置會(huì)直接基于棧進(jìn)行操作。而當(dāng)達(dá)到四個(gè)時(shí),則需要借助元組。

然后多元賦值也是同理,比如 a, b, c = 1, 2, 3,看一下它的字節(jié)碼。

0 RESUME                   0

     2 LOAD_CONST               0 ((1, 2, 3))
     4 UNPACK_SEQUENCE          3
     8 STORE_NAME               0 (a)
    10 STORE_NAME               1 (b)
    12 STORE_NAME               2 (c)
    14 RETURN_CONST             1 (None)

元組直接作為一個(gè)常量被加載進(jìn)來了,然后解包,再依次賦值。運(yùn)行時(shí)棧變化如下:

圖片圖片

沒有任何問題,以上就是多元賦值的原理。

4)a, b, c, d = d, c, b, a 和 a, b, c, d = [d, c, b, a] 有區(qū)別嗎?

答案是沒有區(qū)別,兩者在反編譯之后對應(yīng)的字節(jié)碼指令只有一處不同。

0 RESUME                   0

     2 LOAD_NAME                0 (d)
     4 LOAD_NAME                1 (c)
     6 LOAD_NAME                2 (b)
     8 LOAD_NAME                3 (a)
    10 BUILD_LIST               4
    12 UNPACK_SEQUENCE          4
    16 STORE_NAME               3 (a)
    18 STORE_NAME               2 (b)
    20 STORE_NAME               1 (c)
    22 STORE_NAME               0 (d)
    24 RETURN_CONST             0 (None)

前者是 BUILD_TUPLE,現(xiàn)在變成了 BUILD_LIST,其它部分一模一樣,所以兩者的效果是相同的。當(dāng)然啦,由于元組的構(gòu)建比列表快一些,因此還是推薦第一種寫法。

5)a = b = c = 123 背后的原理是什么?

如果變量 a、b、c 指向的值相同,比如都是 123,那么便可以通過這種方式進(jìn)行鏈?zhǔn)劫x值。那么它背后是怎么做的呢?

0 RESUME                   0

     2 LOAD_CONST               0 (123)
     4 COPY                     1
     6 STORE_NAME               0 (a)
     8 COPY                     1
    10 STORE_NAME               1 (b)
    12 STORE_NAME               2 (c)
    14 RETURN_CONST             1 (None)

出現(xiàn)了一個(gè)新的字節(jié)碼指令 COPY,只要搞清楚它的作用,事情就簡單了。

TARGET(COPY) {
    // 獲取棧底元素,由于當(dāng)前只有一個(gè)元素,所以它也是棧頂元素
    PyObject *bottom = stack_pointer[-(1 + (oparg-1))];
    PyObject *top;
    #line 3364 "Python/bytecodes.c"
    assert(oparg > 0);
    top = Py_NewRef(bottom);
    #line 4636 "Python/generated_cases.c.h"
    // 將元素壓入棧中,也就是將元素拷貝了一份,然后重新入棧
    STACK_GROW(1);
    stack_pointer[-1] = top;
    DISPATCH();
}

所以 COPY 干的事情就是將棧頂元素拷貝一份,再重新壓到棧里面。

圖片圖片

另外不管鏈?zhǔn)劫x值語句中有多少個(gè)變量,模式都是一樣的,我們以 a = b = c = d = e = 123 為例:

0 RESUME                   0

     2 LOAD_CONST               0 (123)
     4 COPY                     1
     6 STORE_NAME               0 (a)
     8 COPY                     1
    10 STORE_NAME               1 (b)
    12 COPY                     1
    14 STORE_NAME               2 (c)
    16 COPY                     1
    18 STORE_NAME               3 (d)
    20 STORE_NAME               4 (e)
    22 RETURN_CONST             1 (None)

將常量 123 壓入運(yùn)行時(shí)棧,然后拷貝一份,賦值給 a;再拷貝一份,賦值給 b;再拷貝一份,賦值給 c;再拷貝一份,賦值給 d;最后自身賦值給 e。

以上就是鏈?zhǔn)劫x值的秘密,其實(shí)沒有什么好神奇的,就是將棧頂元素進(jìn)行拷貝,再依次賦值。

但是這背后有一個(gè)坑,就是給變量賦的值不能是可變對象,否則容易造成 BUG。

a = b = c = {}

a["ping"] = "pong"
print(a)  # {'ping': 'pong'}
print(b)  # {'ping': 'pong'}
print(c)  # {'ping': 'pong'}

雖然 Python 一切皆對象,但對象都是通過指針來間接操作的。所以 COPY 是將字典的地址拷貝一份,而字典只有一個(gè),因此最終 a、b、c 會(huì)指向同一個(gè)字典。

6)a is b 和 a == b 的區(qū)別是什么?

is 用于判斷兩個(gè)變量是不是引用同一個(gè)對象,也就是保存的對象的地址是否相等;而 == 則是判斷兩個(gè)變量引用的對象是否相等,等價(jià)于 a.__eq__(b) 。

Python 的變量在 C 看來只是一個(gè)指針,因此兩個(gè)變量是否指向同一個(gè)對象,等價(jià)于 C 中的兩個(gè)指針存儲(chǔ)的地址是否相等;

而 Python 的 ==,則需要調(diào)用 PyObject_RichCompare,來比較它們指向的對象所維護(hù)的值是否相等。

這兩個(gè)語句的字節(jié)碼指令集只有一處不同:

# a is b
     0 RESUME                   0
 
     2 LOAD_NAME                0 (a)
     4 LOAD_NAME                1 (b)
     6 IS_OP                    0
     8 POP_TOP
    10 RETURN_CONST             0 (None)

     # a == b
     0 RESUME                   0

     2 LOAD_NAME                0 (a)
     4 LOAD_NAME                1 (b)
     6 COMPARE_OP              40 (==)
    10 POP_TOP
    12 RETURN_CONST             0 (None)

我們看到 a is b 調(diào)用的指令是 IS_OP,而 == 調(diào)用的指令是 COMPARE_OP。

// Python 的 is 在 C 的層面就是比較兩個(gè)指針是否相等
TARGET(IS_OP) {
    // 獲取棧頂?shù)膬蓚€(gè)元素
    PyObject *right = stack_pointer[-1];
    PyObject *left = stack_pointer[-2];
    PyObject *b;
    #line 2088 "Python/bytecodes.c"
    // 進(jìn)行比較,即 left == right
    int res = Py_Is(left, right) ^ oparg;
    #line 2902 "Python/generated_cases.c.h"
    Py_DECREF(left);
    Py_DECREF(right);
    #line 2090 "Python/bytecodes.c"
    // 如果相等,結(jié)果為 True,否則為 False
    b = res ? Py_True : Py_False;
    #line 2907 "Python/generated_cases.c.h"
    // 此時(shí)棧里面有兩個(gè)元素,彈出一個(gè),然后將棧頂元素修改為比較結(jié)果
    // 為了方便,你也可以理解為:將棧里的兩個(gè)元素彈出,再將比較結(jié)果入棧
    // 效果上兩者是等價(jià)的
    STACK_SHRINK(1);
    stack_pointer[-1] = b;
    DISPATCH();
}


TARGET(COMPARE_OP) {
    PREDICTED(COMPARE_OP);
    // 獲取棧里的兩個(gè)元素
    PyObject *right = stack_pointer[-1];
    PyObject *left = stack_pointer[-2];
    PyObject *res;
    // ...
    assert((oparg >> 4) <= Py_GE);
    // 調(diào)用 PyObject_RichCompare 函數(shù)進(jìn)行比較
    res = PyObject_RichCompare(left, right, oparg>>4);
    #line 2813 "Python/generated_cases.c.h"
    Py_DECREF(left);
    Py_DECREF(right);
    #line 2038 "Python/bytecodes.c"
    if (res == NULL) goto pop_2_error;
    #line 2818 "Python/generated_cases.c.h"
    // 將比較結(jié)果入棧
    STACK_SHRINK(1);
    stack_pointer[-1] = res;
    next_instr += 1;
    DISPATCH();
}

這里我們再看一下 PyObject_RichCompare 函數(shù),看看底層是怎么比較的。

// Include/object.h
#define Py_LT 0
#define Py_LE 1
#define Py_EQ 2
#define Py_NE 3
#define Py_GT 4
#define Py_GE 5

// Objects/object.c
int _Py_SwappedOp[] = {Py_GT, Py_GE, Py_EQ, Py_NE, Py_LT, Py_LE};
static const char * const opstrings[] = {"<", "<=", "==", "!=", ">", ">="};

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    // ...
    // 調(diào)用了 do_richcompare
    PyObject *res = do_richcompare(tstate, v, w, op);
    _Py_LeaveRecursiveCallTstate(tstate);
    return res;
}

static PyObject *
do_richcompare(PyThreadState *tstate, PyObject *v, PyObject *w, int op)
{
    // 類型對象在底層有一個(gè) tp_richcompare 字段,它負(fù)責(zé)實(shí)現(xiàn)比較邏輯
    // 另外在 Python 里面每個(gè)操作符都對應(yīng)一個(gè)魔法方法
    // 而在底層,所有的比較操作符都由 tp_richcompare 實(shí)現(xiàn)
    richcmpfunc f;  // 比較函數(shù)
    PyObject *res;
    int checked_reverse_op = 0;
    // 如果 v 和 w 不是同一種類型,并且 type(w) 是 type(v) 的子類
    // 那么優(yōu)先查找 type(w) 的 tp_richcompare,如果有則調(diào)用
    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);
    }
    // 否則查找 type(v) 的 tp_richcompare,如果有則調(diào)用
    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    // 前面兩個(gè)條件都不滿足,那么查找 type(w) 的 tp_richcompare
    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);
    }
    // 如果以上條件都不滿足,說明沒有實(shí)現(xiàn)比較操作
    // 那么檢測操作符是否是 == 或 !=
    // 因?yàn)閷τ谶@兩個(gè)操作符,不管什么類型,都是合法的
    // 此時(shí)會(huì)比較它們的內(nèi)存地址
    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:
        // 如果沒實(shí)現(xiàn)比較操作,并且操作符也不是 == 和 !=
        // 那么報(bào)錯(cuò),這兩個(gè)實(shí)例之間無法進(jìn)行比較
        _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;
    }
    return Py_NewRef(res);
}

雖然在 Python 里面用于比較的魔法方法有多個(gè),比如 __eq__、__le__、__gt__ 等等。但在底層,它們都對應(yīng) tp_richcompare,至于具體是哪一種,則由參數(shù)控制。所以我們實(shí)現(xiàn)任意一個(gè)用于比較的魔法方法,底層都會(huì)實(shí)現(xiàn) tp_richcompare。

至于 tp_richcompare 具體支持多少種操作符,則取決于實(shí)現(xiàn)了幾個(gè)魔法方法,比如我們只實(shí)現(xiàn)了 __eq__,但操作符為 Py_ET,那么就會(huì)拋出 Py_NotImplemented。

我們實(shí)際舉個(gè)栗子:

a = 3.14
b = float("3.14")
print(a is b)  # False
print(a == b)  # True

a 和 b 都是 3.14,兩者是相等的,但不是同一個(gè)對象。

反過來也是如此,如果 a is b 成立,那么 a == b 也不一定成立。可能有人好奇,a is b 成立說明 a 和 b 指向的是同一個(gè)對象,那么 a == b 表示該對象和自己進(jìn)行比較,結(jié)果應(yīng)該始終是相等的呀,為啥也不一定成立呢?以下面兩種情況為例:

class Girl:

    def __eq__(self, other):
        return False

g = Girl()
print(g is g)  # True
print(g == g)  # False

__eq__ 返回 False,此時(shí)雖然是同一個(gè)對象,但是兩者不相等。

import math
import numpy as np

a = float("nan")
b = math.nan
c = np.nan

print(a is a, a == a)  # True False
print(b is b, b == b)  # True False
print(c is c, c == c)  # True False

nan 是一個(gè)特殊的浮點(diǎn)數(shù),意思是 not a number(不是一個(gè)數(shù)字),用于表示空值。而 nan 和所有數(shù)字的比較結(jié)果均為 False,即使是和它自身比較。

但需要注意的是,在使用 == 進(jìn)行比較的時(shí)候雖然是不相等的,但如果放到容器里面就不一定了。舉個(gè)例子:

import numpy as np

lst = [np.nan, np.nan, np.nan]
print(lst[0] == np.nan)  # False
print(lst[1] == np.nan)  # False
print(lst[2] == np.nan)  # False
# lst 里面的三個(gè)元素和 np.nan 均不相等

# 但是 np.nan 位于列表中,并且數(shù)量是 3
print(np.nan in lst)  # True
print(lst.count(np.nan))  # 3

出現(xiàn)以上結(jié)果的原因就在于,元素被放到了容器里,而容器的一些 API 在比較元素時(shí)會(huì)先判定地址是否相同,即:是否指向了同一個(gè)對象。如果是,直接認(rèn)為相等;否則,再去比較對象維護(hù)的值是否相等。

可以理解為先進(jìn)行 is 判斷,如果結(jié)果為 True,直接判定兩者相等;如果 is 操作的結(jié)果不為 True,再進(jìn)行 == 判斷。

因此 np.nan in lst 的結(jié)果為 True,lst.count(np.nan) 的結(jié)果是 3,因?yàn)樗鼈儠?huì)先比較對象的地址。地址相同,則直接認(rèn)為對象相等。

在用 pandas 做數(shù)據(jù)處理的時(shí)候,nan 是一個(gè)非常容易坑的地方。

提到 is 和 ==,那么問題來了,在和 True、False、None 比較時(shí),是用 is 還是用 == 呢?

由于 True、False、None 它們不僅是關(guān)鍵字,而且也被看做是一個(gè)常量,最重要的是它們都是單例的,所以我們應(yīng)該用 is 判斷。

另外 is 在底層只需要一個(gè) == 即可完成,這是非常簡單的低級操作,而 Python 的 == 在底層則需要調(diào)用 PyObject_RichCompare 函數(shù)。因此 is 在速度上也更有優(yōu)勢,比函數(shù)調(diào)用要快。

小結(jié)

以上我們就分析了常見的幾個(gè)指令,以及變量賦值的底層邏輯,怎么樣,是不是對 Python 有更深的理解了呢。

責(zé)任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2021-12-09 22:36:30

Java 字節(jié)碼頁緩存

2020-04-21 12:09:47

JVM消化字節(jié)碼

2010-01-19 15:42:30

VB.NET賦值語句

2022-05-05 10:00:53

Kafka分區(qū)分配Linux

2010-03-12 14:28:45

Python if語句

2021-05-28 23:04:23

Python利器執(zhí)行

2009-12-31 11:37:05

MPLS網(wǎng)絡(luò)

2024-11-01 16:05:26

2010-09-06 12:50:09

PPP鏈路

2010-01-06 16:16:14

華為交換機(jī)vlan配置

2016-12-19 14:35:32

Spark Strea原理剖析數(shù)據(jù)

2011-12-01 14:56:30

Java字節(jié)碼

2019-10-30 08:45:21

JS代碼NodeJS

2022-03-30 10:10:17

字節(jié)碼棧空間

2009-09-14 10:35:15

Linq內(nèi)部執(zhí)行原理

2020-09-16 10:31:58

SMTP網(wǎng)絡(luò)電子郵件

2013-09-17 10:35:17

Python執(zhí)行原理

2009-09-07 16:25:14

Linq To SQL

2009-10-21 16:00:26

VB.NET CASE

2010-03-22 12:40:48

Python代碼加密
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 亚洲一区 中文字幕 | 国产伦精品一区二区三区高清 | 亚洲一区二区三区四区五区午夜 | 久久久久久国产精品免费免费狐狸 | 一级特黄a大片 | 91麻豆精品国产91久久久久久久久 | 一二区视频 | 欧洲精品久久久久毛片完整版 | 久久精品中文字幕 | 日韩精品一区二区三区视频播放 | 久热国产在线 | 91精品久久久久久久久久入口 | 国产99久久精品一区二区永久免费 | 三级成人片 | 亚洲人在线观看视频 | 古装人性做爰av网站 | 欧美日韩一区二区在线观看 | 久久久精品久 | 亚洲精品在 | 亚洲视频免费在线观看 | 久久狼人天堂 | av在线播放免费 | 91九色在线观看 | 国产亚洲一区二区在线观看 | 久久精品国产一区二区电影 | 在线a视频网站 | 999精品在线 | 久久综合激情 | 成人精品毛片国产亚洲av十九禁 | 成人一区二区视频 | 在线观看www视频 | 91欧美精品成人综合在线观看 | 五月婷婷婷 | 少妇久久久久 | 国产a级黄色录像 | 日韩一区二区三区精品 | 成人影院在线 | 成人免费视频 | 欧美一区二区在线观看 | 国产成人综合网 | 人人操日日干 |