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

集合支持的操作有哪些,它們是怎么實現的?

開發 前端
假設有一個集合 s,那么 s.add("abc") 最終等價于?set_add_entry(so, "abc", hash("abc")),所以核心邏輯位于 set_add_entry 里面,看一下它的實現,代碼比較長。

1.楔子

本篇文章來聊一聊集合支持的操作,比如元素的添加、刪除,以及集合的擴容等等。并且集合還支持交集、并集、差集等運算,它們又是如何實現的呢?那么就一起來看一看吧。

2.add 方法:添加元素

調用 add 方法可以向集合添加一個元素,在底層會執行 set_add 函數。

// Objects/setobject.c
static PyObject *
set_add(PySetObject *so, PyObject *key)
{   
    // 調用了 set_add_key 函數
    if (set_add_key(so, key))
        return NULL;
    // 返回 None
    Py_RETURN_NONE;
}

static int
set_add_key(PySetObject *so, PyObject *key)
{
    Py_hash_t hash;
    // 計算哈希值,由于字符串內部會緩存自身的哈希值,因此需要判斷一下
    // 如果 key 不是字符串,或者 key 是字符串、但哈希值為 -1(尚未計算過)
    // 那么計算哈希值,但如果計算之后的結果是 -1,說明對象不支持哈希
    if (!PyUnicode_CheckExact(key) ||
        (hash = _PyASCIIObject_CAST(key)->hash) == -1) {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }
    // 調用 set_add_entry
    return set_add_entry(so, key, hash);
}

假設有一個集合 s,那么 s.add("abc") 最終等價于 set_add_entry(so, "abc", hash("abc")),所以核心邏輯位于 set_add_entry 里面,看一下它的實現,代碼比較長。

// Objects/setobject.c
static int
set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
    setentry *table;
    setentry *freeslot;
    setentry *entry;
    size_t perturb;
    size_t mask;
    size_t i;                      
    int probes;
    int cmp;
    // 增加 key 的引用計數,當然這里的 key 指的就是集合的元素
    Py_INCREF(key);

  restart:
    // mask 等于哈希表的容量減 1
    mask = so->mask;
    // 和字典一樣,讓 hash & mask 計算出一個索引
    i = (size_t)hash & mask;
    freeslot = NULL;
    // perturb 初始等于哈希值
    perturb = hash;

    while (1) {
        // 獲取指定的 entry,里面包含了元素 key 和哈希值
        entry = &so->table[i];
        // 關于這一步是做什么的,一會兒解釋
        probes = (i + LINEAR_PROBES <= mask) ? LINEAR_PROBES: 0;
        do {
            // 如果哈希值為 0 并且 key 為 NULL,說明該位置還沒有存儲 entry
            // 此時就找到了合適的位置,跳轉到 found_unused_or_dummy 標簽
            if (entry->hash == 0 && entry->key == NULL)
                goto found_unused_or_dummy;
            // 否則說明該位置已經存儲了 entry,那么判斷哈希值是否相同
            // 如果哈希值不同,那么 key 一定不相同
            // 如果哈希值相同,那么 key 不一定相同
            if (entry->hash == hash) { 
                // 所以當哈希值相等時
                // 還要比較新添加的 key 和已存在的 key 是否相同
                PyObject *startkey = entry->key;
                assert(startkey != dummy);
                // 這里的 startkey 和 key 都是 C 的變量,它們都是指針
                // 如果兩者相等,說明指向的是同一個對象,那么直接判定為相等
                // 于是跳轉到 found_active 標簽
                if (startkey == key)  // 相當于 Python 的 is
                    goto found_active;
                // 如果 startkey 和 key 不等,說明指向的不是同一個對象
                // 那么比較值是否相等,相當于 Python 的 ==
                // 這里是針對字符串的一個快分支
                if (PyUnicode_CheckExact(startkey)
                    && PyUnicode_CheckExact(key)
                    && _PyUnicode_EQ(startkey, key))
                    goto found_active;
                table = so->table;
                Py_INCREF(startkey);
                // 如果 key 不是字符串,則執行通用比較邏輯
                cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
                Py_DECREF(startkey);
                // cmp > 0,說明結果為真,即兩個 key 的值相等
                // 跳轉到 found_active 標簽
                if (cmp > 0)
                    goto found_active;
                // cmp < 0 表示執行比較操作時出現錯誤,基本不會發生
                if (cmp < 0)
                    goto comparison_error;
                // 到這里說明兩個 key 雖然映射出的索引是一樣的,但它們的值不相等
                // 那么要怎么辦呢?顯然要看下一個 entry 是否可用
                // 然后下面這三行代碼估計讓人有些費解,如果在查詢的過程中
                // 哈希表擴容了,或者 key 發生了改變,那么跳轉到 restart 標簽重新執行
                // 但因為 GIL 的存在,實際不會發生
                if (table != so->table || entry->key != startkey)
                    goto restart;
                mask = so->mask;
            }
            /* 如果是 Unused 態的 entry,那么 
             *     entry->key == NULL
             *     entry->hash == 0
             *
             * 如果是 Dummy 態的 entry,那么
             *     entry->key == dummy
             *     entry->hash == -1
             *
             * 如果是 Active 態的 entry,那么
             *     entry->key == some key
             *     entry->hash == some hash
             */
            // 說明 entry 處于 dummy 態,將它賦值給 freeslot
            else if (entry->hash == -1) {
                assert (entry->key == dummy);
                freeslot = entry;
            }
            // 嘗試下一個 entry
            entry++;
        } while (probes--);
        // 改變規則,重新映射,直到映射出一個可用的位置
        perturb >>= PERTURB_SHIFT;
        i = (i * 5 + 1 + perturb) & mask;
    }
  
  found_unused_or_dummy:
    // 如果 freeslot == NULL,說明沒有撞上 Dummy 態的 entry
    // 跳轉到 found_unused 標簽
    if (freeslot == NULL)
        goto found_unused;
    // 否則說明撞上了 Dummy 態的 entry
    // 集合的長度加 1,或者說 Active 態的 entry 個數加 1
    so->used++;
    // 更新 key 和 hash
    freeslot->key = key;
    freeslot->hash = hash;
    return 0;

  found_unused:
    // 執行到這里,說明找到了新的可用位置
    // 那么不光 used 要加 1,fill 也要加 1
    so->fill++;
    so->used++;
    // 更新 key 和 hash
    entry->key = key;
    entry->hash = hash;
    // 如果哈希表的 entry 的個數(Active 態 + Dummy 態)沒超過 mask * 3/5
    // 那么目前的容量是合理的,直接返回
    if ((size_t)so->fill*5 < mask*3)
        return 0;
    // 否則進行擴容,因為擴容的時候會丟棄 Dummy 的 entry
    // 所以擴容之后的容量取決于 used,而不是 fill
    // 如果 used 大于 50000,那么 2 倍擴容,否則 4 倍擴容
    return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);

  found_active:
    // 執行到這里,說明添加的元素已經存在了
    // 那么減少 key 的引用計數,然后返回
    Py_DECREF(key);
    return 0;

  comparison_error:
    // 執行比較操作時出現錯誤,應該拋出異常,但這一步基本不會發生
    Py_DECREF(key);
    return -1;
}

所以整個過程和字典是類似的,依舊是將哈希值和 mask 按位與,得到索引,通過索引找到對應的 entry。接下來對 entry 分情況討論。

如果 entry->hash == 0 并且 entry->key == NULL。

說明一上來就找到了可用的 entry,那么直接跳轉到指定標簽,然后修改 entry 的 key 和 hash 字段,這樣新元素就添加成功了。

如果 entry->hash == hash。

說明找到的 entry 處于 Active 態,那么比較兩個 key 是否相等。如果相等,證明添加的元素已存在,則不插入,直接減少引用計數,因為不是字典,不存在更新一說。但如果兩個 key 不相等,說明出現索引沖突,那么要映射出一個新的索引,并且映射的方式和字典也是一樣的。

perturb >>= PERTURB_SHIFT;
i = (i * 5 + 1 + perturb) & mask;

但字典和集合有一處不同,就是集合這里多了一個 do while 循環。

// Objects/setobject.c
#define LINEAR_PROBES 9

static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash) {
    // ...
    while (1) {
        // 當前映射出的索引為 i
        entry = &so->table[i];
        // 如果 i + 9 沒有超過 mask,那么 probes 等于 9,否則等于 0
        probes = (i + LINEAR_PROBES <= mask) ? LINEAR_PROBES: 0;
        // 因此這里 do while 內部的邏輯,要么執行 1 次,要么執行 10 次
        do {
            // ...
            entry++;
        } while (probes--); 
        perturb >>= PERTURB_SHIFT;
        i = (i * 5 + 1 + perturb) & mask;
    }
    // ... 
}

當映射出的索引相同、但 key 不相同時,說明出現了索引沖突,對于字典來說,會立即重新映射,找到一個新的索引。

而集合由于只用一個數組存儲,可以有更好的做法。我們知道 CPU 是有緩存的,像 L1 Cache 加載數據會一次性加載 64 字節,稱為一個 cache line。所以通過索引遍歷包含 16 個 int32 的數組,每次 i++ 和每次 i += 4 的耗時是差不多的。

對于集合來說,因為映射出的索引是隨機的,使得對應的 entry 可能不在 cache 中,從而導致 CPU 下一次要重新讀取。所以 Python 引入了 LINEAR_PROBES,從當前的 entry 開始,向后查找 9 個 entry。如果還找不到可用位置,然后才重新計算,從而提高 cache 的穩定性。

如果 entry->key == dummy,或者說 entry->hash == -1。

說明撞上了一個 Dummy 態的 entry,但估計有人又注意到了一個問題。

圖片圖片

就是在發現 Dummy 態的 entry 之后,為啥沒有立即跳轉到 found_unused_or_dummy 標簽,而是要繼續循環呢?

很簡單,假設向集合添加了一個元素 x,又添加了一個元素 y,但 y 和 x 映射出的索引相同,于是嘗試下一個 entry。所以在添加 y 的時候,會形成一條探測鏈,對應的元素就是 x->y。然后再將 x 刪除,那么 x->y 就變成了 dummy->y。這時候如果再重新添加一個元素 y,那么肯定會撞上 Dummy 態的 entry,于是 dummy->y 就變成了 y->y。

所以當發現 Dummy 態的 entry 之后,如果立即跳轉,那么會無法消除集合的重復元素。因此正確的做法是先用變量保存起來,這里賦值給了 freeslot,然后繼續查找。如果找到了相同的元素,那么就不添加了,因為集合中的元素是唯一的。但如果最后找到的 entry 的 key 為空,說明元素不存在,此時才能跳轉到 found_unused_or_dummy 標簽,然后對 freeslot 進行判斷。如果不為空,說明撞上了 Dummy 態的 entry,那么直接復用該 entry 即可。

以上就是集合添加元素的過程,當然如果找到的是 Unused 態的 entry,則還要判斷容量的問題。如果 Active 態 + Dummy 態的 entry 個數不小于 3/5*mask,那么擴容,擴容的規則是判斷 Active 態的 entry 個數是否大于 50000,是的話就 2 倍擴容,否則 4 倍擴容;

pop 方法:彈出元素

調用 pop 方法,可以從集合中彈出一個元素,在底層會執行 set_pop 方法。

// Objects/setobject.c
static PyObject *
set_pop(PySetObject *so, PyObject *Py_UNUSED(ignored))
{
    // so->table 是指向 entry 數組首元素的指針
    // so->finger 是做什么的,稍后解釋,總之它是一個整數
    // so->mask 等于 entry 數組的長度減 1,用于將取模運算優化成按位與運算

    // 因此 so->finger & so->mask 會得到一個 0 ~ mask 之間的整數,我們記為 n
    // 顯然這里的變量 entry 會指向 entry 數組中索引 n 的元素
    setentry *entry = so->table + (so->finger & so->mask);
    // 變量 limit 則指向 entry 數組中最后一個元素(索引為 mask)
    setentry *limit = so->table + so->mask;
    PyObject *key;
    // 如果集合長度為 0,那么 pop 方法會拋出 KeyError
    if (so->used == 0) {
        PyErr_SetString(PyExc_KeyError, "pop from an empty set");
        return NULL;
    }
    // entry 有三種狀態,但顯然彈出的 entry 一定是 Active 態
    // 所以如果 entry 處于 Unused 或 Dummy 態,直接下一輪循環
    while (entry->key == NULL || entry->key==dummy) {
        entry++;
        // 我們記 so->finger & so->mask 的結果為 n
        // 所以相當于從 entry 數組中索引為 n 的位置開始遍歷
        // 如果遍歷到最后一個位置,也沒找到 Active 態的 entry,那么從頭開始遍歷
        if (entry > limit)
            entry = so->table;  // 讓變量 entry 指向 entry 數組的首元素
        // 所以不難發現,整個過程是先遍歷 entry 數組中 [n: limit] 的部分
        // 如果沒有找到 Active 態 entry,那么將 entry 重置為 so->table,從頭開始遍歷
        // 因為執行到這里,說明 so->used 大于 0,即集合的長度大于 0
        // 那么當 entry > limit 時,在 entry 數組 [0: n] 的部分,一定存在 Active 態的 entry
    }
    // pop 方法會返回彈出的元素,所以獲取 entry->key
    key = entry->key;
    // 元素被彈出了,對應的 entry 要進行偽刪除,所謂的偽刪除就是設置一個特殊的墓碑值
    // 所以將 entry->key 設置為 dummy,將 entry->hash 設置為 -1
    entry->key = dummy;
    entry->hash = -1;
    // 集合的長度減 1
    so->used--;
    // 將 finger 更新為被刪除的 entry 在 entry 數組中的索引加 1
    so->finger = entry - so->table + 1;   /* next place to start */
    return key;
}

所以刪除的過程還是很簡單的,如果不考慮 finger 字段,你就可以簡單理解為遍歷整個 entry 數組,找到 Active 態的 entry,然后刪除即可。只是這么做會導致每次 pop 時,都要重頭開始遍歷數組。

而當引入了 finger 字段之后,由于該字段初始為 0,所以第一次 pop 時,會從數組的頭部開始遍歷。假設刪除的是數組中索引為 n 的 entry,那么刪除之后 finger 字段會被賦值為 n + 1,那么下一次 pop 就會從數組中索引為 n + 1 的 entry 開始遍歷。

我們通過 ctypes 來驗證這一點:

from ctypes import *

class PyObject(Structure):
    _fields_ = [
        ("ob_refcnt", c_ssize_t),
        ("ob_type", c_void_p),
    ]

class SetEntry(Structure):
    _fields_ = [
        ("key", POINTER(PyObject)),
        ("hash", c_longlong)
    ]

class PySetObject(PyObject):
    _fields_ = [
        ("fill", c_ssize_t),
        ("used", c_ssize_t),
        ("mask", c_ssize_t),
        ("table", POINTER(SetEntry)),
        ("hash", c_long),
        ("finger", c_ssize_t),
        ("smalltable", (SetEntry * 8)),
        ("weakreflist", POINTER(PyObject)),
    ]


s = {11, 22, 33, 44}
# 獲取 PySetObject 結構體實例
py_set_obj = PySetObject.from_address(id(s))
# 遍歷 smalltable,打印索引和 key 的哈希值
# 對于整數來說,它的哈希值等于自身
for index, entry in enumerate(py_set_obj.smalltable):
    print(index, entry.hash)
"""
0 0
1 33
2 0
3 11
4 44
5 0
6 22
7 0
"""
# finger 初始為 0,因此在 pop 元素的時候會從頭開始遍歷數組
# 找到第一個 Active 態的 entry
print(py_set_obj.finger)
"""
0
"""

# 顯然第一次 pop 出的元素是 33
print(s.pop())
"""
33
"""
for index, entry in enumerate(py_set_obj.smalltable):
    print(index, entry.hash)
"""
0 0
1 -1
2 0
3 11
4 44
5 0
6 22
7 0
"""
# 因為被偽刪除了
# 所以索引為 1 的 entry->key 會被設置為 NULL,entry->hash 被設置為 -1
# 至于 finger 則等于 1 + 1,下一次 pop 時,會從索引為 2 的位置開始遍歷
print(py_set_obj.finger)
"""
2
"""

# 那么同理,再次 pop 的時候,會彈出 11,然后 finger 變為 3 + 1 = 4
print(s.pop())
"""
11
"""
print(py_set_obj.finger)
"""
4
"""

以上就是 finger 字段的作用,它避免了每次都要從頭遍歷 entry 數組。從這里也不難發現,當一個集合不斷執行 pop 方法,將所有元素依次彈出時,這些元素的順序和直接遍歷 entry 數組拿到的元素的順序是一致的。

我們依舊舉例說明:

item1 = 22333
item2 = 177
item3 = 520
item4 = 10086
# 將它們映射成索引,由于是 4 個元素,因此哈希表容量為 8
index1 = item1 & 7
index2 = item2 & 7
index3 = item3 & 7
index4 = item4 & 7
print(index1, index2, index3, index4)
"""
5 1 0 6
"""
# 所以如果將 item1、item2、item3、item4 放到集合中
# 不管怎么排列,最終都是下面這個結果
# item3 會位于 entry 數組中索引為 0 的位置
# item2 會位于 entry 數組中索引為 1 的位置
# item1 會位于 entry 數組中索引為 5 的位置
# item4 會位于 entry 數組中索引為 6 的位置

# 所以不管是彈出元素,還是遍歷元素,亦或是直接打印集合
# 元素順序一定是 item3、item2、item1、item4
s1 = {item1, item2, item3, item4}
s2 = {item2, item1, item4, item3}
s3 = {item3, item1, item2, item4}
s4 = {item4, item3, item2, item1}
print(s1)  # {520, 177, 22333, 10086}
print(s2)  # {520, 177, 22333, 10086}
print(s3)  # {520, 177, 22333, 10086}
print(s4)  # {520, 177, 22333, 10086}
print(
    item3, item2, item1, item4
)  # 520 177 22333 10086

怎么樣,是不是對集合又有了更深刻的認識了呢?

remove 方法:刪除指定元素

remove 方法可以接收參數,刪除集合中指定的元素。除了 remove,還有一個 discard 方法,這兩個方法的作用一模一樣,都是用來刪除指定元素。區別就是當刪除的元素不存在時,remove 方法會拋出 KeyError,而 discard 方法不會。

remove 方法在底層對應 set_remove 函數,discard 方法在底層對應 set_discard 函數,而 set_remove 函數只比 set_discard 函數多了一個 if 判斷,我們來看一下。

以上是 set_remove 函數,注意圖中綠色方框的部分,如果要刪除的元素不存在,那么 rv 會等于 DISCARD_NOTFOUND,于是拋出 KeyError。

如果將綠色方框里的 if 邏輯刪掉,得到的就是 set_discard 函數的源碼。所以這兩個函數做的事情是一樣的,區別就是 set_remove 會多做一層檢測,當刪除的元素不存在時,set_remove 會主動拋出一個 KeyError,而 set_discard 函數則什么也不做。

所以這里我們只看 set_remove 函數即可。

// Objects/setobject.c
#define DISCARD_NOTFOUND 0
#define DISCARD_FOUND 1

static PyObject *
set_remove(PySetObject *so, PyObject *key)
{
    // 要刪除的 key,或者說元素
    // 當然啦,從 C 的層面來看,刪除 key 其實就是刪除數組中該 key 對應的 entry
    // 只不過這個刪除是偽刪除,即寫入一個特殊的墓碑值
    PyObject *tmpkey;
    int rv;
    // rv 表示刪除結果,顯然刪除邏輯由 set_discard_key 函數實現
    // 如果 rv < 0,表示刪除元素時出現錯誤,比如傳入了一個不可哈希的對象
    // 如果 rv == 0,表示要刪除的元素在集合中不存在
    // 如果 rv == 1,表示成功將元素從集合中刪除
    rv = set_discard_key(so, key);
    if (rv < 0) {
        // 當傳入一個不可哈希對象時,會拋出 TypeError
        // 我們知道集合也是不可哈希的,但如果要刪除的 key 是集合類型
        // 那么解釋器會額外做一個兜底操作,我們一會兒通過 Python 代碼演示
        if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError))
            return NULL;
        // 將回溯棧里的異常清空
        PyErr_Clear();
        // 基于集合里的元素創建不可變集合
        tmpkey = make_new_set(&PyFrozenSet_Type, key);
        if (tmpkey == NULL)
            return NULL;
        // 然后嘗試刪除這個不可變集合,如果還刪除失敗,則報錯
        rv = set_discard_key(so, tmpkey);
        Py_DECREF(tmpkey);
        if (rv < 0)
            return NULL;
    }
    // 如果 rv == DISCARD_NOTFOUND,表示要刪除的元素不存在
    if (rv == DISCARD_NOTFOUND) {
        _PyErr_SetKeyError(key);  // 拋出 KeyError
        return NULL;
    }
    Py_RETURN_NONE;
}

我們看到當元素刪除失敗時,如果 key 是集合類型,那么解釋器會做一個兜底操作,這是什么意思呢?我們演示一遍。

try:
    s = {[1, 2, 3]}  # 列表不可哈希
except TypeError as e:
    print(e)
"""
unhashable type: 'list'
"""

try:
    s = {{1, 2, 3}}  # 同樣,集合也不可哈希
except TypeError as e:
    print(e)
"""
unhashable type: 'set'
"""

# 當我們嘗試 remove 列表時,依舊會拋出相同的錯誤
s = {1, 2, 3}
try:
    s.remove([])
except TypeError as e:
    print(e)
"""
unhashable type: 'list'
"""

# 但 remove 一個集合就不同了
s = {
    frozenset({1, 2, 3}),
    frozenset({4, 5, 6}),
}
# 不可變集合是可哈希對象,因此它可以放在集合中,也可以被刪除
s.remove(frozenset({1, 2, 3}))
# 但這里應該會拋出 TypeError: unhashable type: 'set'
# 而事實上異常也確實產生了,保存在回溯棧中,但是從源碼中我們看到,解釋器會多做一個檢測
# 如果刪除的 key 是集合類型,并且棧里的異常是 TypeError,那么將異常清空
# 然后基于集合創建不可變集合,并嘗試刪除這個不可變集合
s.remove({4, 5, 6})
# 所以這里 s.remove({4, 5, 6}) 等價于 s.remove(frozenset({4, 5, 6}))
print(s)
"""
set()
"""
# 我們看到 s 里面的兩個不可變集合被刪除了

好,我們回到 set_remove 函數,它在刪除元素時會調用 set_discard_key 函數,顯然刪除指定元素的核心邏輯位于此函數中,我們看一下它做了什么。

// Objects/setobject.c
static int
set_discard_key(PySetObject *so, PyObject *key)
{
    Py_hash_t hash;
    // 檢測 key 是否為字符串,因為字符串內部已經保存了自身的哈希值
    // 如果不是字符串,或者是字符串、但哈希值尚未計算(hash 字段等于 -1)
    // 那么計算 key 的哈希值
    if (!PyUnicode_CheckExact(key) ||
        (hash = _PyASCIIObject_CAST(key)->hash) == -1) {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }
    // 調用 set_discard_entry 函數
    // 傳入三個參數:集合、要刪除的 key、以及 key 的哈希值
    return set_discard_entry(so, key, hash);
}


static int
set_discard_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
    setentry *entry;
    PyObject *old_key;
    // 將 key 映射成索引,并獲取該索引對應的 entry 的指針
    entry = set_lookkey(so, key, hash);
    // 因為 entry 數組申請好之后,內部的每個元素都擁有一塊合法的內存
    // 所以指針不可能為 NULL,如果為 NULL,證明內部出問題了
    if (entry == NULL)  // 基本不會發生
        return -1;
    // 如果 entry->key 為 NULL,證明要刪除的 key 不存在,返回 DISCARD_NOTFOUND
    // 當 set_remove 函數發現返回的是 DISCARD_NOTFOUND,會拋出 KeyError
    if (entry->key == NULL)
        return DISCARD_NOTFOUND;
    // 否則說明 key 存在
    old_key = entry->key;
    // 因為被刪除了,所以將 entry->key、entry->hash 設置為 dummy 和 -1
    entry->key = dummy;
    entry->hash = -1;
    // 集合長度減 1
    so->used--;
    // 減少 key 指向對象的引用計數,因為集合不再持有對它的引用
    Py_DECREF(old_key);
    // 返回 DISCARD_FOUND
    return DISCARD_FOUND;
}

以上就是 set_remove 函數刪除指定元素的具體細節,邏輯并不復雜。但是里面出現了一個 set_lookkey 函數,它的作用是將哈希值映射成索引,返回指定的 entry。

顯然這些代碼在介紹 set_add 函數時已經見過了,邏輯是類似的。如果 entry->key 為空,說明找到了 Unused 態的 entry,即 key 不存在,那么直接將 entry 返回即可。

如果 entry->key 不為空,那么比較 entry->hash 和傳入的 hash 是否相等,如果哈希值不相等,那么 key 一定不相等,說明出現了索引沖突。當然啦,如果 entry 處于 Dummy 態,那么哈希值肯定也不相等,但不管哪一種,都要重新映射。

如果哈希值相等,那么比較 key 是否相等,如果 key 相等,說明查找的 key 存在于集合中,那么返回對應的 entry。如果 key 不相等,則重新映射。

copy 方法:拷貝一個集合

調用 copy 方法可以拷貝一個集合,在底層會執行 set_copy 方法。

// Objects/setobject.c
static PyObject *
set_copy(PySetObject *so, PyObject *Py_UNUSED(ignored))
{
    return make_new_set_basetype(Py_TYPE(so), (PyObject *)so);
}

static PyObject *
make_new_set_basetype(PyTypeObject *type, PyObject *iterable)
{
    if (type != &PySet_Type && type != &PyFrozenSet_Type) {
        if (PyType_IsSubtype(type, &PySet_Type))
            type = &PySet_Type;
        else
            type = &PyFrozenSet_Type;
    }
    return make_new_set(type, iterable);
}

static PyObject *
make_new_set(PyTypeObject *type, PyObject *iterable)
{
    assert(PyType_Check(type));
    PySetObject *so;
    // 為集合申請內存
    so = (PySetObject *)type->tp_alloc(type, 0);
    if (so == NULL)
        return NULL;
    // 字段初始化,顯然剛創建的集合的容量為 8
    so->fill = 0;
    so->used = 0;
    so->mask = PySet_MINSIZE - 1;
    so->table = so->smalltable;
    so->hash = -1;
    so->finger = 0;
    so->weakreflist = NULL;
    // 調用 set_update_internal 函數,將可迭代對象的元素添加到集合中
    if (iterable != NULL) {
        if (set_update_internal(so, iterable)) {
            Py_DECREF(so);
            return NULL;
        }
    }

    return (PyObject *)so;
}

static int
set_update_internal(PySetObject *so, PyObject *other)
{
    // 參數 so 表示集合,other 表示可迭代對象
    PyObject *key, *it;
    // 如果 other 的類型也是集合(或者不可變集合)
    // 那么調用 set_merge 函數
    if (PyAnySet_Check(other))
        return set_merge(so, other);
    
    // 否則檢測 other 是否為字典
    if (PyDict_CheckExact(other)) {
        PyObject *value;
        Py_ssize_t pos = 0;
        Py_hash_t hash;
        Py_ssize_t dictsize = PyDict_GET_SIZE(other);
        if (dictsize < 0)
            return -1;
        // 判斷 so->fill + dictsize 是否達到了 so->mask 的 3/5
        // 如果達到了,那么擴容
        if ((so->fill + dictsize)*5 >= so->mask*3) {
            if (set_table_resize(so, (so->used + dictsize)*2) != 0)
                return -1;
        }
        // 遍歷字典,將字典的 key 和 hash 包裝成 entry,添加到數組中,value 丟棄
        // 這里調用的是 set_add_entry 函數,我們介紹集合的 add 方法時說過
        while (_PyDict_Next(other, &pos, &key, &value, &hash)) {
            if (set_add_entry(so, key, hash))
                return -1;
        }
        return 0;
    }
    // 到這里說明 other 不是字典,那么迭代出來的整體就是 key
    // 基于可迭代對象創建迭代器
    it = PyObject_GetIter(other);
    if (it == NULL)
        return -1;
    // 將元素迭代出來
    while ((key = PyIter_Next(it)) != NULL) {
        // 調用 set_add_key 函數,它內部會先計算 key 的哈希值
        // 然后調用 set_add_entry 函數,添加元素
        if (set_add_key(so, key)) {
            Py_DECREF(it);
            Py_DECREF(key);
            return -1;
        }
        Py_DECREF(key);
    }
    Py_DECREF(it);
    if (PyErr_Occurred())
        return -1;
    return 0;
}

以上就是集合的 copy 方法的底層實現,非常簡單。說白了就是先創建一個新的集合,然后調用 set_update_internal 函數將老集合里面的元素拷貝過去。當然啦,該函數可以拷貝任意可迭代對象里的元素,不僅僅是集合。只是當可迭代對象是集合時,會單獨調用 set_merge 函數,如果不是集合,那么會直接遍歷。

update 方法:合并多個可迭代對象

調用 update 方法,可以合并多個可迭代對象,舉例說明。

s = {1, 2, 3}
s.update([4, 5, 6], (7, 8, 9))
print(s)
"""
{1, 2, 3, 4, 5, 6, 7, 8, 9}
"""

相信你已經知道底層是怎么做的了,獲取每個可迭代對象,然后調用 set_update_internal 函數即可。那么底層是不是這么做的呢?我們來看一下,update 方法在底層對應 set_update 函數。

// Objects/setobject.c
static PyObject *
set_update(PySetObject *so, PyObject *args)
{
    Py_ssize_t i;
    // args 是一個元組
    for (i=0 ; i<PyTuple_GET_SIZE(args) ; i++) {
        // 遍歷拿到每個可迭代對象
        PyObject *other = PyTuple_GET_ITEM(args, i);
        // 調用 set_update_internal 函數
        if (set_update_internal(so, other))
            return NULL;
    }
    Py_RETURN_NONE;
}

跟我們分析的一樣,非常簡單。

另外集合還有一個 union 方法,功能和 update 方法類似,但它會返回一個新的集合。

s1 = {1, 2, 3}
s1.update([4, 5, 6], (7, 8, 9))
print(s1)
"""
{1, 2, 3, 4, 5, 6, 7, 8, 9}
"""

s2 = {1, 2, 3}
s2_new = s2.union([4, 5, 6], (7, 8, 9))
print(s2)
print(s2_new)
"""
{1, 2, 3}
{1, 2, 3, 4, 5, 6, 7, 8, 9}
"""

update 方法會原地修改,而 union 方法會返回新的集合,不會影響原有的集合。

其它的一些方法

集合還有一些常用的方法,只不過我們更傾向于使用操作符的形式。

  • s1 & s2:對兩個集合做交集運算,返回新的集合,里面包含同時出現在 s1 和 s2 當中的元素;
  • s1 | s2:對兩個集合做并集運算,返回新的集合,里面包含出現在 s1 或 s2 當中的元素;
  • s1 - s2:對兩個集合做差集運算,返回新的集合,里面包含出現在 s1 當中、但沒有出現在 s2 當中的元素;
  • s1 ^ s2:對兩個集合做對稱差集運算,返回新的集合,里面包含只出現在 s1 當中、或只出現在 s2 當中的元素;
  • s1 == s2:判斷兩個集合的元素是否完全相同;
  • s1 >= s2:判斷 s2 是否是 s1 的子集,如果是,那么 s2 - s1 == {}。
  • s1 <= s2:判斷 s1 是否是 s2 的子集,如果是,那么 s1 - s2 == {}。
  • s1 > s2:判斷 s2 是否是 s1 的真子集;
  • s1 < s2:判斷 s1 是否是 s2 的真子集;

注意:在使用這些操作符時,兩側的 s1 和 s2 都要求是集合類型。但如果使用操作符對應的方法,那么則不要求 s2 是集合類型,只要是可迭代對象即可。

# 做交集運算
s = {"a", "b", "c"}
print(s.intersection("bcd"))
"""
{'b', 'c'}
"""
# print(s & "bcd")  # TypeError

這些方法都非常的有用,可以自己測試一下,加深一遍印象。至于這些方法的底層實現,感興趣也可以去 Objects/setobject.c 中探索一番,方法都定義在 set_methods 數組中。

這里我們就以集合的交集運算為例,看一下實現過程。

// Objects/setobject.c
static int
set_contains_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
    // 判斷集合是否包含某個元素
    setentry *entry;
    // 調用 set_lookkey,返回 entry
    // 如果 entry->key 不為空,證明元素存在,否則不存在
    entry = set_lookkey(so, key, hash);
    if (entry != NULL)
        return entry->key != NULL;
    return-1;
}

// 兩個集合做交集運算,返回新的集合
static PyObject *
set_intersection(PySetObject *so, PyObject *other)
{
    PySetObject *result;
    PyObject *key, *it, *tmp;
    Py_hash_t hash;
    int rv;
    // 快分支:如果兩個集合相等,那么直接把其中一個拷貝一份
    if ((PyObject *)so == other)
        return set_copy(so, NULL);
    // 否則創建一個新的空集合
    result = (PySetObject *)make_new_set_basetype(Py_TYPE(so), NULL);
    if (result == NULL)
        returnNULL;
    // 如果 other 是集合
    if (PyAnySet_Check(other)) {
        Py_ssize_t pos = 0;
        setentry *entry;
        // 如果 len(other) > len(so),那么兩者交換位置
        // 也就是遍歷元素較少的集合
        if (PySet_GET_SIZE(other) > PySet_GET_SIZE(so)) {
            tmp = (PyObject *)so;
            so = (PySetObject *)other;
            other = tmp;
        }
        // 遍歷集合 other
        while (set_next((PySetObject *)other, &pos, &entry)) {
            key = entry->key;
            hash = entry->hash;
            Py_INCREF(key);
            // 判斷 key 是否存在于集合 so 中
            rv = set_contains_entry(so, key, hash);
            if (rv < 0) {
                Py_DECREF(result);
                Py_DECREF(key);
                returnNULL;
            }
            // 如果存在,那么添加到新集合 result 中
            if (rv) {
                if (set_add_entry(result, key, hash)) {
                    Py_DECREF(result);
                    Py_DECREF(key);
                    returnNULL;
                }
            }
            Py_DECREF(key);
        }
        return (PyObject *)result;
    }
    // 如果 other 不是集合,那么獲取它的迭代器
    it = PyObject_GetIter(other);
    if (it == NULL) {
        Py_DECREF(result);
        returnNULL;
    }
    // 直接迭代內部的元素,以下邏輯和上面類似
    while ((key = PyIter_Next(it)) != NULL) {
        hash = PyObject_Hash(key);
        if (hash == -1)
            goto error;
        // 如果 key 在 so 中存在,那么添加到 result 中
        rv = set_contains_entry(so, key, hash);
        if (rv < 0)
            goto error;
        if (rv) {
            if (set_add_entry(result, key, hash))
                goto error;
            if (PySet_GET_SIZE(result) >= PySet_GET_SIZE(so)) {
                Py_DECREF(key);
                break;
            }
        }
        Py_DECREF(key);
    }
    Py_DECREF(it);
    if (PyErr_Occurred()) {
        Py_DECREF(result);
        returnNULL;
    }
    return (PyObject *)result;
  error:
    Py_DECREF(it);
    Py_DECREF(result);
    Py_DECREF(key);
    returnNULL;
}

以上就是集合的交集運算,至于其它的運算操作也是類似的,感興趣可以看一下。

小結

關于集合相關的內容我們就介紹完了,當然到目前為止,Python 的內置數據結構也基本介紹完了。回顧一下我們介紹了哪些數據結構:

  • 浮點數;
  • 整數;
  • 復數;
  • 布爾值
  • None;
  • 切片;
  • bytes 對象;
  • bytearray 對象;
  • 字符串;
  • 列表;
  • 元組;
  • 字典;
  • 集合;

以上這些結構都是內置的,當然還有一些數據結構是定義在標準庫里面的,我們后面再說。

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

2024-07-29 12:27:55

列表對象元素

2024-07-30 10:31:03

2024-08-22 10:11:00

字典取值源碼

2021-09-26 10:57:16

集合操作場景

2022-02-19 22:42:25

加密貨幣代幣區塊鏈

2024-12-05 15:44:13

工廠模式接口

2020-09-11 06:44:29

Python增強算術

2024-06-14 10:26:30

2023-10-24 15:56:23

2016-06-08 16:06:56

云計算

2022-02-16 22:50:28

JVMJDKJRE

2023-05-17 15:36:57

2020-11-17 14:28:56

數據中心

2013-06-13 10:54:46

iOS7WWDC蘋果

2015-06-17 16:50:24

華曦達

2021-07-09 11:59:25

Redis有序集合

2021-07-01 19:35:29

智能電表物聯網智慧城市

2021-04-13 06:39:13

代碼重構code

2021-03-10 07:20:43

if-else靜態代碼

2022-03-22 10:52:02

Redis變慢服務器
點贊
收藏

51CTO技術棧公眾號

成人毛片18女人毛片| 岛国精品一区二区三区| 91网页在线观看| 国产精品一区免费视频| 午夜精品久久久久久久99黑人| 免费的av网站| 素人一区二区三区| 一区二区三区精品在线| 久久久久久艹| 亚洲网站免费观看| 亚洲无毛电影| 亚洲欧美在线免费| 男插女视频网站| 亚洲天堂导航| 亚洲精品综合在线| 久久伊人一区二区| 国产白浆在线观看| 久久久成人网| 欧美丰满少妇xxxxx| 新91视频在线观看| 哺乳一区二区三区中文视频| 欧美亚洲国产一区二区三区| 男人添女人荫蒂免费视频| 大胆av不用播放器在线播放| 国产成人精品亚洲777人妖 | 好吊妞www.84com只有这里才有精品 | 一级黄色在线观看| 一区视频在线看| 日韩亚洲精品电影| 99久久精品免费视频| 日韩成人18| 欧美日韩国产免费一区二区| 波多野结衣乳巨码无在线| 成人在线观看免费网站| 国产精品系列在线| 国产精品视频免费一区| av网站免费播放| 免费观看一级特黄欧美大片| 欧美在线中文字幕| 中文字幕第28页| 国产精品88久久久久久| 日韩精品最新网址| 91高清国产视频| 亚洲精品永久免费视频| 一个色在线综合| 亚洲精品中字| 久久精品蜜桃| 91碰在线视频| 国产日韩二区| aa视频在线免费观看| 日韩av一区二| 青青草原成人在线视频| 欧美一区二区三区观看| 成人激情电影在线| 精品视频—区二区三区免费| 农村末发育av片一区二区| 国产福利亚洲| 欧美中文字幕一二三区视频| 日韩精品xxxx| ****av在线网毛片| 亚洲综合免费观看高清在线观看 | 成人精品影院| 日韩精品在线第一页| 手机精品视频在线| 亚洲18在线| 欧美裸体bbwbbwbbw| 国产精品亚洲a| 黄色综合网址| 一本大道综合伊人精品热热| 日韩精品在线中文字幕| 青春草在线视频| 亚洲精品欧美综合四区| 天天综合中文字幕| 毛片在线播放a| 国产精品国产三级国产普通话三级| 久久久久久国产精品免费免费| 99热这里只有精品5| 美女视频网站久久| 国产精品羞羞答答| av资源免费看| 成人一区二区在线观看| 国产精品久久国产三级国电话系列| av一级黄色片| 成人免费视频播放| 精品综合久久| 成年在线观看免费人视频| 中文字幕第一区综合| 亚洲一区二区免费视频软件合集 | 色琪琪综合男人的天堂aⅴ视频| 三年中国中文观看免费播放| 97精品国产福利一区二区三区| 上原亚衣av一区二区三区| 日本女人性生活视频| 日韩影院二区| 九九热精品视频在线播放| 久草视频在线资源站| 亚洲美女一区| 国产成人欧美在线观看| 在线观看日批视频| 风间由美一区二区三区在线观看| 国产在线一区二| 二区在线观看| 亚洲欧美电影一区二区| aa在线观看视频| 蜜桃成人精品| 欧美另类z0zxhd电影| 久久无码人妻精品一区二区三区| 红桃成人av在线播放| 久久综合久久美利坚合众国| 五月天婷婷丁香| 日日夜夜精品视频天天综合网| 国产一区欧美二区三区| 天堂成人在线观看| 欧美国产精品一区二区| 国产精品igao激情视频| 不卡av播放| 日韩一区二区三区在线观看| 久久精品国产亚洲av麻豆| av在线不卡顿| 97精品视频在线观看| 日韩精品在线一区二区三区| 国产aⅴ综合色| 日本一区二区久久精品| 免费在线看污片| 色哟哟日韩精品| 91超薄肉色丝袜交足高跟凉鞋| 欧美女优在线视频| 欧美刺激性大交免费视频| 69国产精品视频免费观看| 国产一区二区影院| 日韩久久精品一区二区三区| sm在线观看| 欧美日韩你懂得| 国产夫妻性爱视频| 亚洲人成免费网站| 国产精品美女免费视频| 黄频网站在线观看| 亚洲视频一区二区在线观看| 男人天堂网视频| 爱高潮www亚洲精品| 最新亚洲国产精品| 91porny在线| 国产精品18久久久久久久久久久久| 日本亚洲欧洲精品| 深夜在线视频| 亚洲国产日韩欧美在线动漫| 91香蕉一区二区三区在线观看| 久久精品国产清高在天天线| 96pao国产成视频永久免费| 五月婷在线视频| 亚洲成人一区二区在线观看| 中文字幕55页| 亚洲精品久久久| 国产精品视频久久| 国产福利在线视频| 色琪琪一区二区三区亚洲区| 亚洲色偷偷色噜噜狠狠99网| 综合激情一区| 亚洲精品日韩激情在线电影| 黄色成人影院| 欧美乱妇15p| 国产高清一区二区三区四区| 日本不卡123| 神马影院一区二区| free欧美| 国产一区二区三区在线看| 毛片基地在线观看| 久久婷婷国产综合精品青草| 欧美a v在线播放| 精品中国亚洲| 26uuu亚洲国产精品| 五月婷婷久久久| 欧美日韩一区二区在线 | 亚洲精品69| 日韩在线免费视频观看| 在线播放精品视频| 国产精品久久久久影院老司| 午夜激情影院在线观看| 911久久香蕉国产线看观看| 91免费看网站| 密臀av在线播放| 亚洲精品网站在线播放gif| 国产精品第5页| 久久婷婷成人综合色| 亚洲五月天综合| 91久久电影| 99视频在线| 一区二区电影免费观看| 日韩精品视频在线观看免费| 国产a∨精品一区二区三区仙踪林| 99re在线视频这里只有精品| 国产一区视频免费观看| 日韩欧美二区| 91久久精品一区二区别| 24小时免费看片在线观看| 日韩成人av在线播放| 国产又大又粗又爽| 自拍偷拍亚洲综合| 免费a v网站| 奇米一区二区三区| 超碰成人在线免费观看| 日本欧美韩国国产| 国产精品揄拍500视频| 1024在线播放| 亚洲精品中文字幕女同| 波多野结衣在线电影| 亚洲免费在线观看| 自拍偷拍中文字幕| 久草精品在线观看| 黄色成人在线看| 四季av在线一区二区三区 | 国产精品x8x8一区二区| 国产成人精品免高潮在线观看| 黄色免费网站在线| 亚洲精品久久久久久久久久久久久| 91黑人精品一区二区三区| 亚洲理论在线观看| 男女做爰猛烈刺激| 国产乱理伦片在线观看夜一区| 少妇性饥渴无码a区免费| 国产精品7m凸凹视频分类| 九九九九精品| 国产精品久久久久久久久久齐齐 | 三级成人在线| 久久久久久久久中文字幕| 日本综合在线| 亚洲免费小视频| 亚洲av无码国产精品永久一区| 在线精品视频一区二区三四| 久久无码精品丰满人妻| 国产精品毛片无遮挡高清| 日韩少妇一区二区| 看电视剧不卡顿的网站| 青青在线视频免费| 亚洲啪啪91| 色哺乳xxxxhd奶水米仓惠香| 九热爱视频精品视频| 国产精品12| 国产精品美女久久久久人| 国产成人精品综合| 看黄在线观看| 久久久欧美一区二区| 巨大荫蒂视频欧美另类大| 亚洲男人7777| 亚洲国产日韩在线观看| 在线成人高清不卡| 少妇一级淫片日本| 狠狠色噜噜狠狠狠狠97| 久久这里只有精品国产| 亚洲免费观看视频| 1024在线看片| 国产欧美一区视频| 国产精品成人一区二区三区电影毛片| 国内精品久久久久影院一蜜桃| 日本超碰在线观看| 秋霞午夜av一区二区三区 | 蜜臀av性久久久久蜜臀av麻豆| 国产老熟妇精品观看| 亚洲经典自拍| 人妻少妇精品无码专区二区| 国内揄拍国内精品久久| 人人妻人人澡人人爽欧美一区| 亚洲精品久久久| 午夜欧美性电影| 国产精品国内免费一区二区三区| 亚洲精品tv久久久久久久久| 成人同人动漫免费观看| 日本一区二区在线视频| 欧美精品一二| 一区不卡视频| 91精品久久久久久久久久不卡| 日本高清xxxx| 午夜精品网站| 免费在线观看污污视频| 一精品久久久| 粉嫩av一区二区三区天美传媒| 亚洲一级毛片| 可以看毛片的网址| 9色精品在线| 欧美伦理视频在线观看| 日本美女一区二区三区视频| www.精品在线| 黑人巨大精品欧美黑白配亚洲| 涩涩网站在线看| av不卡免费在线观看| 日本精品在线观看视频| 国产精品超碰97尤物18| 国产成人无码aa精品一区| 亚洲国产成人高清精品| 五月天婷婷久久| 欧美色视频一区| 国产av无码专区亚洲a∨毛片| 欧美成人video| 亚洲伦理在线观看| 日韩av在线网页| 国产视频网站在线| 久久精品人人做人人爽| 国产美女一区视频| 欧美做受高潮电影o| 日本中文字幕一区二区| 亚洲自拍小视频| 欧美调教网站| 在线观看国产一区| 影音先锋久久精品| 污网站在线免费| eeuss影院一区二区三区| 国产午夜福利一区| 亚洲一区二区在线播放相泽| 天天干天天干天天| 制服丝袜亚洲色图| 天天干天天操av| www.午夜精品| 96av在线| 国产精品igao视频| 欧美电影院免费观看| 久久婷婷人人澡人人喊人人爽| 欧美电影《睫毛膏》| 日韩中字在线观看| 狠狠色狠狠色综合日日91app| 成人午夜精品无码区| 国产精品久久久久久久第一福利 | 色综合久久精品| 亚洲av无码片一区二区三区| 日日摸夜夜添一区| 小草在线视频免费播放| 91精品久久久久久蜜桃| 欧美色网址大全| 夜夜添无码一区二区三区| 久久国产麻豆精品| 手机免费看av| 精品福利樱桃av导航| 99在线精品视频免费观看20| 亚洲人成电影在线观看天堂色| 国产污视频在线播放| 亚洲在线视频观看| 日韩免费视频| 精品视频无码一区二区三区| 99久久综合国产精品| 欧美爱爱小视频| 欧美男同性恋视频网站| 国内在线精品| 69久久夜色精品国产69乱青草| 国产精品nxnn| 性高湖久久久久久久久aaaaa| 看片网站欧美日韩| 日韩女同一区二区三区 | 成人午夜亚洲| 日韩精品av一区二区三区| 日韩中文欧美在线| 国产又粗又猛又爽视频| 精品福利视频导航| 人妻少妇精品无码专区| 欧美黑人xxxx| 精品国产三区在线| 日本精品免费视频| 极品少妇xxxx精品少妇偷拍| 精品国产大片大片大片| 欧美日韩高清一区二区三区| 91九色在线porn| 国产欧美日韩免费| 日韩免费特黄一二三区| 一个色综合久久| 亚洲人吸女人奶水| 国产三级视频在线播放| 最近日韩中文字幕中文| 成人免费毛片嘿嘿连载视频…| 日韩欧美电影一区二区| 日韩av二区在线播放| 亚洲天堂最新地址| 8v天堂国产在线一区二区| 国产秀色在线www免费观看| 成人免费网站在线| 韩国亚洲精品| 7788色淫网站小说| 欧美午夜激情视频| yourporn在线观看视频| 国产精品亚洲精品| 亚洲视频电影在线| 免费看三级黄色片| 亚洲一二三四久久| 大片免费播放在线视频| 国产精品自产拍高潮在线观看| 999精品一区| 国产裸体视频网站| 精品久久久久久久久久久| 免费资源在线观看| 国产精品丝袜白浆摸在线| 99久精品视频在线观看视频| 欧美xxxxxbbbbb| 午夜激情综合网| 超碰国产在线观看| 2014亚洲精品| 国产视频一区在线观看一区免费| 波多野吉衣中文字幕| 欧美另类久久久品| 理论片午夜视频在线观看| 亚洲日本精品国产第一区| 国产成人高清视频| 日韩人妻精品中文字幕| 久久精品国产亚洲7777|