精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

開發 前端
雖然在 Python 里面用于比較的魔法方法有多個,比如 __eq__、__le__、__gt__ 等等。但在底層,它們都對應 tp_richcompare,至于具體是哪一種,則由參數控制。所以我們實現任意一個用于比較的魔法方法,底層都會實現 tp_richcompare。

楔子

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

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

常用指令

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

我們舉例說明:

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 對應兩條字節碼指令。

  • LOAD_CONST:加載一個常量,這里是 16;
  • STORE_FAST:在局部作用域中創建一個局部變量,這里是 age;

print(age) 對應四條字節碼指令。

  • LOAD_GLOBAL:在局部作用域中加載一個全局變量或內置變量,這里是 print;
  • LOAD_FAST:在局部作用域中加載一個局部變量,這里是 age;
  • CALL:函數調用;
  • POP_TOP:從棧頂彈出返回值;

print(name) 對應兩條字節碼指令。

  • LOAD_GLOBAL:在局部作用域中加載一個全局變量或內置變量,這里是 print;
  • LOAD_GLOBAL:在局部作用域中加載一個全局變量或內置變量,這里是 name;
  • CALL:函數調用;
  • POP_TOP:從棧頂彈出返回值;

name = "古明地戀" 對應兩條字節碼指令。

  • LOAD_CONST:加載一個常量,這里是 "古明地戀";
  • STORE_GLOBAL:在局部作用域中創建一個 global 關鍵字聲明的全局變量,這里是 name;

這些指令非常常見,因為它們和常量、變量的加載,以及變量的定義密切相關,你寫的任何代碼在反編譯之后都少不了它們的身影。

注:不管加載的是常量、還是變量,得到的永遠是指向對象的指針。

變量賦值的具體細節

這里再通過變量賦值感受一下字節碼的執行過程,首先關于變量賦值,你平時是怎么做的呢?

圖片圖片

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

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

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

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)

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

圖片圖片

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

TARGET(SWAP) {
    // 獲取棧頂元素
    PyObject *top = stack_pointer[-1];
    // oparg 表示交換的元素個數
    // 所以 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"
    // 將棧頂元素和棧頂元素進行交換
    stack_pointer[-1] = bottom;
    stack_pointer[-(2 + (oparg-2))] = top;
    DISPATCH();
}

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

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

老規矩,還是查看字節碼,因為一切真相都隱藏在字節碼當中。

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)

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

所以 SWAP 適用于兩個或三個變量之間的交換,兩個變量交換很好理解,關鍵是三個變量交換,依舊只需要一個 SWAP 指令,因為中間的元素是不需要動的。

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

我們還是看一下字節碼。

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)

將等號右邊的變量,按照從左往右的順序,依次壓入棧中,但此時沒有直接將棧里面的元素做交換,而是構建一個元組。因為往棧里面壓入了四個元素,所以 BUILD_TUPLE 后面的 oparg 是 4,表示構建長度為 4 的元組。

TARGET(BUILD_TUPLE) {
    // stack_pointer 指向運行時棧的棧頂,oparg 表示運行時棧的元素個數
    // 那么 stack_pointer - oparg 便指向運行時棧的棧底
    PyObject **values = (stack_pointer - oparg);
    PyObject *tup;  // 指向創建的元組
    #line 1489 "Python/bytecodes.c"
    // 運行時棧本質上就是個數組,索引從小到大的方向表示棧底到棧頂的方向
    // 當執行 a, b, c, d = d, c, b, a 時,會將右側的變量依次入棧
    // 運行時棧里的元素從棧底到棧頂依次是 d、c、b、a
    // 拷貝數組(運行時棧)里的元素,創建元組,結果是 (d, c, b, a)
    tup = _PyTuple_FromArraySteal(values, oparg);
    if (tup == NULL) { STACK_SHRINK(oparg); goto error; }
    #line 2038 "Python/generated_cases.c.h"
    // 清空運行時棧
    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 開始,將數組里的元組依次拷貝到元組中
    for (Py_ssize_t i = 0; i < n; i++) {
        PyObject *item = src[i];
        dst[i] = item;
    }
    _PyObject_GC_TRACK(tuple);
    return (PyObject *)tuple;
}

此時棧里面只有一個元素,指向一個元組。接下來是 UNPACK_SEQUENCE,負責對序列進行解包,它的指令參數也是 4,表示要解包的序列的長度為 4,我們來看看它的邏輯。

TARGET(UNPACK_SEQUENCE) {
    PREDICTED(UNPACK_SEQUENCE);
    // 獲取棧頂元素,也就是上一步創建的元組:(d, c, b, a)
    PyObject *seq = stack_pointer[-1];
    #line 1057 "Python/bytecodes.c"
    // ...
    // 將元組里的元素彈出,并依次入棧,此時方向和之前是相反的
    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();
}

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

圖片圖片

可以看到當交換的變量多了之后,不會直接在運行時棧里面操作,而是將棧里面的元素挨個彈出、構建元組(準確的說應該是先構建元組,然后再清空運行時棧)。接著再按照指定順序,將元組里面的元素重新壓到棧里面。

當然不管是哪一種做法,Python 在進行變量交換時所做的事情是不變的,核心分為三步。

  • 1)將等號右邊的變量,按照從左往右的順序,依次壓入棧中;
  • 2)對運行時棧里面元素的順序進行調整;
  • 3)將運行時棧里面的元素挨個彈出,還是按照從左往右的順序,再依次賦值給等號左邊的變量;

只不過當變量不多時,調整元素位置會直接基于棧進行操作。而當達到四個時,則需要借助元組。

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

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)

元組直接作為一個常量被加載進來了,然后解包,再依次賦值。運行時棧變化如下:

圖片圖片

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

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

答案是沒有區別,兩者在反編譯之后對應的字節碼指令只有一處不同。

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,現在變成了 BUILD_LIST,其它部分一模一樣,所以兩者的效果是相同的。當然啦,由于元組的構建比列表快一些,因此還是推薦第一種寫法。

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

如果變量 a、b、c 指向的值相同,比如都是 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 STORE_NAME               2 (c)
    14 RETURN_CONST             1 (None)

出現了一個新的字節碼指令 COPY,只要搞清楚它的作用,事情就簡單了。

TARGET(COPY) {
    // 獲取棧底元素,由于當前只有一個元素,所以它也是棧頂元素
    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 干的事情就是將棧頂元素拷貝一份,再重新壓到棧里面。

圖片圖片

另外不管鏈式賦值語句中有多少個變量,模式都是一樣的,我們以 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 壓入運行時棧,然后拷貝一份,賦值給 a;再拷貝一份,賦值給 b;再拷貝一份,賦值給 c;再拷貝一份,賦值給 d;最后自身賦值給 e。

以上就是鏈式賦值的秘密,其實沒有什么好神奇的,就是將棧頂元素進行拷貝,再依次賦值。

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

a = b = c = {}

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

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

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

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

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

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

這兩個語句的字節碼指令集只有一處不同:

# 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 調用的指令是 IS_OP,而 == 調用的指令是 COMPARE_OP。

// Python 的 is 在 C 的層面就是比較兩個指針是否相等
TARGET(IS_OP) {
    // 獲取棧頂的兩個元素
    PyObject *right = stack_pointer[-1];
    PyObject *left = stack_pointer[-2];
    PyObject *b;
    #line 2088 "Python/bytecodes.c"
    // 進行比較,即 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"
    // 如果相等,結果為 True,否則為 False
    b = res ? Py_True : Py_False;
    #line 2907 "Python/generated_cases.c.h"
    // 此時棧里面有兩個元素,彈出一個,然后將棧頂元素修改為比較結果
    // 為了方便,你也可以理解為:將棧里的兩個元素彈出,再將比較結果入棧
    // 效果上兩者是等價的
    STACK_SHRINK(1);
    stack_pointer[-1] = b;
    DISPATCH();
}


TARGET(COMPARE_OP) {
    PREDICTED(COMPARE_OP);
    // 獲取棧里的兩個元素
    PyObject *right = stack_pointer[-1];
    PyObject *left = stack_pointer[-2];
    PyObject *res;
    // ...
    assert((oparg >> 4) <= Py_GE);
    // 調用 PyObject_RichCompare 函數進行比較
    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"
    // 將比較結果入棧
    STACK_SHRINK(1);
    stack_pointer[-1] = res;
    next_instr += 1;
    DISPATCH();
}

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

// 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)
{
    // ...
    // 調用了 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)
{
    // 類型對象在底層有一個 tp_richcompare 字段,它負責實現比較邏輯
    // 另外在 Python 里面每個操作符都對應一個魔法方法
    // 而在底層,所有的比較操作符都由 tp_richcompare 實現
    richcmpfunc f;  // 比較函數
    PyObject *res;
    int checked_reverse_op = 0;
    // 如果 v 和 w 不是同一種類型,并且 type(w) 是 type(v) 的子類
    // 那么優先查找 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;
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    // 否則查找 type(v) 的 tp_richcompare,如果有則調用
    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    // 前面兩個條件都不滿足,那么查找 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);
    }
    // 如果以上條件都不滿足,說明沒有實現比較操作
    // 那么檢測操作符是否是 == 或 !=
    // 因為對于這兩個操作符,不管什么類型,都是合法的
    // 此時會比較它們的內存地址
    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;
    }
    return Py_NewRef(res);
}

雖然在 Python 里面用于比較的魔法方法有多個,比如 __eq__、__le__、__gt__ 等等。但在底層,它們都對應 tp_richcompare,至于具體是哪一種,則由參數控制。所以我們實現任意一個用于比較的魔法方法,底層都會實現 tp_richcompare。

至于 tp_richcompare 具體支持多少種操作符,則取決于實現了幾個魔法方法,比如我們只實現了 __eq__,但操作符為 Py_ET,那么就會拋出 Py_NotImplemented。

我們實際舉個栗子:

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

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

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

class Girl:

    def __eq__(self, other):
        return False

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

__eq__ 返回 False,此時雖然是同一個對象,但是兩者不相等。

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 是一個特殊的浮點數,意思是 not a number(不是一個數字),用于表示空值。而 nan 和所有數字的比較結果均為 False,即使是和它自身比較。

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

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 里面的三個元素和 np.nan 均不相等

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

出現以上結果的原因就在于,元素被放到了容器里,而容器的一些 API 在比較元素時會先判定地址是否相同,即:是否指向了同一個對象。如果是,直接認為相等;否則,再去比較對象維護的值是否相等。

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

因此 np.nan in lst 的結果為 True,lst.count(np.nan) 的結果是 3,因為它們會先比較對象的地址。地址相同,則直接認為對象相等。

在用 pandas 做數據處理的時候,nan 是一個非常容易坑的地方。

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

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

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

小結

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

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2021-12-09 22:36:30

Java 字節碼頁緩存

2020-04-21 12:09:47

JVM消化字節碼

2010-01-19 15:42:30

VB.NET賦值語句

2022-05-05 10:00:53

Kafka分區分配Linux

2010-03-12 14:28:45

Python if語句

2021-05-28 23:04:23

Python利器執行

2009-12-31 11:37:05

MPLS網絡

2024-11-01 16:05:26

2010-01-06 16:16:14

華為交換機vlan配置

2010-09-06 12:50:09

PPP鏈路

2016-12-19 14:35:32

Spark Strea原理剖析數據

2011-12-01 14:56:30

Java字節碼

2019-10-30 08:45:21

JS代碼NodeJS

2022-03-30 10:10:17

字節碼??臻g

2013-09-17 10:35:17

Python執行原理

2009-09-14 10:35:15

Linq內部執行原理

2020-09-16 10:31:58

SMTP網絡電子郵件

2009-10-21 16:00:26

VB.NET CASE

2009-09-07 16:25:14

Linq To SQL

2010-03-22 12:40:48

Python代碼加密
點贊
收藏

51CTO技術棧公眾號

欧美嫩在线观看| 大桥未久av一区二区三区中文| 亚洲免费电影一区| 久久久久久久久久久久91| 麻豆视频在线| 99这里只有久久精品视频| 日韩av日韩在线观看| 999精品久久久| 女同另类激情重口| 欧美精品乱码久久久久久按摩 | 人妻va精品va欧美va| 久久精品一区二区国产| 欧美精品一二区| 熟女少妇一区二区三区| 精品久久国产一区| 在线看国产一区二区| 免费人成自慰网站| 在线观看av黄网站永久| 丰满少妇xoxoxo视频| 国产一区二区观看| 亚洲免费专区| www.日本不卡| 成人精品久久av网站| 久草国产精品视频| 欧美+日本+国产+在线a∨观看| 日韩精品黄色网| 日韩黄色一区二区| 国产精品毛片aⅴ一区二区三区| 欧美色播在线播放| 成年人网站国产| 26uuu亚洲电影在线观看| 国产亚洲成av人在线观看导航 | y111111国产精品久久久| 日本韩国精品在线| 国产福利视频在线播放| 国产后进白嫩翘臀在线观看视频| 国产精品乱码妇女bbbb| 日韩av电影免费观看| 性感美女一级片| av激情综合网| 国产视频精品网| 亚洲欧美另类日韩| 国产91精品欧美| 91亚洲永久免费精品| 亚洲熟妇av乱码在线观看| 久久国产日韩| 日本欧美国产在线| 青青视频在线免费观看| 久久精品男女| 国产精品九九久久久久久久| 中文在线第一页| 久久国产主播| 国产精品久久久久久久久久尿| 二区视频在线观看| 老司机精品导航| 国产精品成av人在线视午夜片| 一级成人黄色片| 久久国产精品毛片| 日产精品久久久一区二区福利| 亚洲GV成人无码久久精品| 亚洲欧美成人综合| 国产成人久久久精品一区| 天天综合久久综合| 免费日本视频一区| 成人中心免费视频| 精品久久在线观看| 成人福利在线看| 九九九九精品| 二人午夜免费观看在线视频| 国产精品欧美综合在线| 日韩视频在线免费播放| 日本三级在线观看网站 | 日本视频www色| 美女免费视频一区二区| 91色中文字幕| 人妻va精品va欧美va| 久久久久久久久久电影| 亚洲在线欧美| 日韩专区av| 欧美三级xxx| 日本高清一区二区视频| 视频二区欧美| 亚洲欧美精品suv| av资源在线免费观看| 欧美日韩亚洲一区二区三区在线| 午夜精品久久久久久久白皮肤 | 欧美一卡在线观看| 特级西西人体4444xxxx| 国产精品免费不| 久久久精品免费| 日韩经典在线观看| 麻豆视频一区二区| 国产精品区一区二区三在线播放 | 亚洲国产成人在线| 久久福利一区二区| 成人在线爆射| 精品日韩一区二区| 韩国三级hd中文字幕| 欧美精品国产| 国产精品第100页| 亚洲伦理在线观看| 中文av字幕一区| 一区二区传媒有限公司| av国产精品| 亚洲男人的天堂在线| 性欧美videos| 日本在线不卡一区| 亚洲免费观看| 久久精品视频网站| 国产婷婷色一区二区在线观看| 韩国视频一区二区| 欧美中日韩一区二区三区| 亚洲男同gay网站| 欧美日韩精品一二三区| 亚洲一区二区乱码| 欧美日韩国产一区精品一区| 国产精品久久久久久久久久久不卡| 亚洲黄色在线观看视频| 中文字幕欧美区| www.中文字幕在线| www.国产精品一区| 久久综合五月天| 精品乱码一区内射人妻无码| 91天堂素人约啪| 日韩欧美猛交xxxxx无码| 九七电影院97理论片久久tvb| 日韩成人在线视频| 国产精品99精品无码视| 国产精品资源在线看| 亚洲欧洲日本国产| 福利一区二区免费视频| 亚洲女人被黑人巨大进入al| 中文字幕一区二区三区精品| 国产99久久久国产精品免费看| 在线国产伦理一区| 99riav视频一区二区| 亚洲欧美国产精品久久久久久久| 日韩三级视频在线| 国产+成+人+亚洲欧洲自线| 久久天天东北熟女毛茸茸| 2019中文亚洲字幕| 日韩有码在线播放| 夜夜躁很很躁日日躁麻豆| 国产三级一区二区三区| 老司机午夜av| 国产一区二区三区四区| 日本午夜人人精品| 福利视频在线导航| 精品视频在线免费观看| 日本一区二区视频在线播放| 欧美96一区二区免费视频| 日韩久久不卡| 欧美97人人模人人爽人人喊视频| 在线视频欧美日韩| 91成品人影院| 亚洲精选视频免费看| 亚洲热在线视频| 黄色av一区| 久久久久se| 欧洲一区二区三区精品| 视频直播国产精品| 99久久久国产精品无码免费 | 久久久久久久久久久网| 成人性生交大合| 色综合久久久久无码专区| 性欧美lx╳lx╳| 国产精品久久久久久久久借妻| av资源种子在线观看| 91精品国产美女浴室洗澡无遮挡| 欧美国产日韩综合| 91在线一区二区三区| 成人精品视频一区二区| 91精品一区国产高清在线gif| 97自拍视频| 欧美日韩在线观看首页| 一区二区欧美亚洲| www日本在线| 欧美视频在线观看免费| 五月婷六月丁香| 国产高清精品在线| 欧美黄网站在线观看| 欧美激情理论| 黄色91av| a一区二区三区亚洲| 97人人爽人人喊人人模波多| 国产高清视频在线播放| 欧美一二三区在线| 在线观看日韩中文字幕| ㊣最新国产の精品bt伙计久久| 性色av蜜臀av浪潮av老女人| 日本视频一区二区| www.成年人视频| 日韩欧美电影| 国产精品久久久久久久天堂第1集| **在线精品| 九九热这里只有在线精品视| 精品无人乱码| 精品久久久久久综合日本欧美| 久久久精品毛片| 亚洲无线码一区二区三区| 免费视频91蜜桃| 不卡视频一二三四| 奇米777在线| 日韩福利电影在线| 欧美激情 国产精品| 亚洲精品二区三区| 日韩在线国产| 国产日韩三级| 亚洲aaa激情| 久久婷婷五月综合色丁香| 91精品国产色综合久久不卡98口| 精品视频在线一区二区| 亚洲香蕉在线观看| 五月婷婷免费视频| 国产偷自视频区视频一区二区| 亚洲欧美精品在线| 亚洲av无码乱码国产精品久久| 欧美影院精品一区| 久久亚洲天堂网| 亚洲综合激情网| 国产97免费视频| 亚洲国产高清aⅴ视频| 国产男女猛烈无遮挡a片漫画| 国产iv一区二区三区| 免费av不卡在线| 日本一区中文字幕| 日韩欧美在线免费观看视频| 99精品国产一区二区青青牛奶| 日本一级淫片演员| 99欧美视频| 色一情一乱一伦一区二区三欧美| 色爱av综合网| 久久99精品久久久久久三级 | 中文字幕求饶的少妇| 久久久久9999亚洲精品| 99re久久精品国产| 成人免费不卡视频| 国产ts在线观看| 国产大陆精品国产| 无码国产精品久久一区免费| 狠狠色伊人亚洲综合成人| 亚洲综合欧美在线| 毛片av中文字幕一区二区| www.99r| 精品一区二区在线视频| 嫩草视频免费在线观看| 国产一区在线精品| 国产不卡的av| 成人爱爱电影网址| 日本免费福利视频| 久久久影院官网| 精品一区二区三区蜜桃在线| 国产精品麻豆99久久久久久| 国产3级在线观看| 亚洲欧美日韩久久| 久久网一区二区| 午夜精品福利一区二区三区av| 亚洲欧美在线视频免费| 精品欧美激情精品一区| 亚洲精品成人在线视频| 欧美日韩一区不卡| 国产强被迫伦姧在线观看无码| 日韩区在线观看| 日韩一区免费视频| 亚洲欧美一区二区三区情侣bbw| 国产黄在线观看| 色噜噜狠狠狠综合曰曰曰| caoporn免费在线视频| 久久久免费观看视频| 欧美激情网站| 国产精品亚洲网站| 久久精品一级| 精品国产中文字幕| 欧美日韩在线播放视频| 亚洲欧洲国产精品久久| 欧美三级在线| 不要播放器的av网站| 国产在线精品视频| 538国产视频| 国产精品美女久久久久久久久久久| 久久精品视频免费在线观看| 激情成人中文字幕| 91亚洲精品国偷拍自产在线观看| 日韩精品专区在线影院观看| 男女av在线| 欧美成人一区在线| 午夜影院在线播放| 91手机视频在线观看| 国产精品色呦| 一区二区不卡视频| 国产日韩亚洲欧美精品| 国产成人美女视频| 99精品视频在线播放观看| 五月婷婷综合激情网| 精品国产福利视频| 国产又粗又猛又色又| 亚洲第五色综合网| 免费在线你懂的| 26uuu另类亚洲欧美日本一| 麻豆国产一区| 亚洲精品成人三区| 亚洲精品少妇| 手机在线播放av| 国产精品网站在线观看| 男人的天堂一区二区| 欧美一级黄色录像| 91精品国产综合久久久久久豆腐| 午夜精品久久17c| 美女久久精品| 亚洲欧洲一区二区福利| 久久精品91| 国产精品久久久久久亚洲色| 国产精品成人免费精品自在线观看| 午夜影院在线看| 欧美va亚洲va国产综合| 日本亚洲精品| 国产精品jvid在线观看蜜臀| 国内露脸中年夫妇交换精品| 可以免费看的黄色网址| 久久精品国产精品青草| 男人操女人动态图| 欧美日韩一区二区三区| 亚洲国产精品久久久久久6q| 久久精品视频在线观看| jizz欧美| 偷拍视频一区二区| 日韩和的一区二区| aaaaa级少妇高潮大片免费看| 亚洲国产日产av| 免费观看成年人视频| 欧美激情一级精品国产| 美国十次综合久久| 男人j进女人j| 国产成人精品亚洲日本在线桃色| 青青操在线播放| 欧美亚洲高清一区| 国产二区视频在线观看| 国产精品看片资源| 欧美日韩国产免费观看视频| 日韩在线xxx| 国产午夜精品理论片a级大结局| 丁香社区五月天| 国产一区二区三区免费视频| 污污网站免费观看| 在线欧美不卡| 国产精品扒开腿做爽爽爽a片唱戏| 洋洋成人永久网站入口| 亚洲春色一区二区三区| 欧美激情第6页| 欧美日韩一本| 欧美黄色一级片视频| 中文字幕第一页久久| 伊人网av在线| 久久艳片www.17c.com| 天堂精品久久久久| av网站手机在线观看| 99久久免费视频.com| 国产午夜免费福利| 中文字幕亚洲情99在线| 日韩在线电影| 人妻无码一区二区三区四区| 成人在线视频首页| 久久一区二区三区视频| 一区二区福利视频| 亚洲一区二区小说| 无码熟妇人妻av在线电影| 91亚洲精品久久久蜜桃网站| 天天爱天天做天天爽| 日韩一区二区久久久| 亚洲大奶少妇| 成人小视频在线看| 国产精品福利一区| 国产色视频在线| 91精品国产91久久久久福利| 亚洲人挤奶视频| 天堂av在线8| 婷婷国产v国产偷v亚洲高清| 超碰免费在线观看| 91偷拍精品一区二区三区| 日韩午夜av| 天天操天天摸天天舔| 精品裸体舞一区二区三区| 自拍偷拍亚洲视频| www.-级毛片线天内射视视| 99久久精品国产导航| 亚洲一二区视频| 97视频在线观看免费| 日韩情爱电影在线观看| 久久人妻少妇嫩草av蜜桃| 欧美综合天天夜夜久久| 羞羞视频在线观看免费| 日韩av不卡在线播放| 国产不卡一区视频| 久草热在线观看| 午夜精品一区二区三区在线| 第四色成人网| 亚洲av网址在线| 91精品国产乱码| **在线精品| 黄色一级视频在线播放|