<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
有幾天沒有更新 Python 文章了,本次我們來聊一下 Python 的集合是怎麼實現的?之前我們介紹過字典的實現原理,它底層是基於雜湊表實現的,而集合也是如此。
並且字典和集合實現的雜湊表是一樣的,在計算雜湊值、解決索引衝突等方面,兩者沒有任何區別。唯一的區別就是儲存的 entry 不同,字典的 entry 裡面包含了 key、value 和 key 的雜湊值,而集合的 entry 裡面只包含 key 和 key 的雜湊值。
事實上,集合就類似於沒有value的字典。
那麼集合都有哪些用處呢?
1)去重
chars = ["a", "b", "a", "c", "c"] print( list(set(chars)) ) # ['b', 'a', 'c']
再比如你需要監聽一個佇列,處理接收到的訊息,但每一條訊息都有一個編號,要保證具有相同編號的訊息只能被處理一次,要怎麼做呢?
顯然集合此時就派上用場了,我們可以建立一個集合,每來一條訊息,就檢測它的編號是否在集合中。如果存在,則說明訊息已經被處理過了,忽略掉;如果不存在,說明訊息還沒有被處理,那麼就將它的編號新增到集合中,然後處理訊息。
這裡暫時不考慮消費失敗等情況,我們假設每條訊息都是能處理成功的。
2)判斷某個序列是否包含指定的多個元素
data = ["S", "A", "T", "O", "R", "I"] # 現在要判斷 data 是否包含 "T"、"R" 和 "I" # 如果使用列表的話 print( "T" in data and "R" in data and "I" in data ) # True # 顯然這是比較麻煩的,於是我們可以使用集合 print( set(data) >= {"T", "R", "I"} ) # True
同理,基於此方式,我們也可以檢測一個字典是否包含指定的多個 key。
data = { "name": "satori", "age": 17, "gender": "female" } # 判斷字典是否包含 name、age、gender 三個 key print( data.keys() >= {"name", "age", "gender"} ) # True # 字典的 keys 方法會返回一個 dict_keys 物件 # 該物件具備集合的性質,可以直接和集合進行運算
顯然對於這種需求,有了集合就方便多了。
然後我們來羅列一下集合支援的 API,在使用集合的時候要做到心中有數。
# 如果要建立一個空集合,那麼要使用 set() # 寫成 {} 的話,直譯器會認為這是一個空字典 s = {1, 2, 3} # 新增元素,時間複雜度是 O(1) s.add(4) print(s) # {1, 2, 3, 4} # 刪除指定的元素,如果元素不存在則報出 KeyError # 時間複雜度為 O(1) s.remove(2) print(s) # {1, 3, 4} # 刪除指定的元素,如果元素不存在則什麼也不做 # 時間複雜度為 O(1) s.discard(666) print(s) # {1, 3, 4} # 隨機彈出一個元素並返回 # 時間複雜度為 O(1) print(s.pop()) # 1 print(s) # {3, 4} # 清空一個集合 s.clear() print(s) # set() # 還有一些 API,但我們更推薦使用操作符的方式 # 兩個集合取交集 print({1, 2} & {2, 3}) # {2} # 兩個集合取並集 print({1, 2} | {2, 3}) # {1, 2, 3} # 兩個集合取差集 # s1 - s2,返回在 s1、但不在 s2 當中的元素 print({1, 2, 3} - {2, 3, 4}) # {1} # 兩個集合取對稱差集 # s1 ^ s2,返回既不在 s1、也不在 s2 當中的元素 print({1, 2, 3} ^ {2, 3, 4}) # {1, 4} # 判斷兩個集合是否相等,也就是內部的元素是否完全一致 # 順序無所謂,只比較元素是否全部相同 print({1, 2, 3} == {3, 2, 1}) # True print({1, 2, 3} == {1, 2, 4}) # False # 判斷一個集合是否包含另一個集合的所有元素 # 假設有兩個集合 s1 和 s2: # 如果 s1 的元素都在 s2 中,那麼 s2 >= s1; # 如果 s2 的元素都在 s1 中,那麼 s1 >= s2; # 如果 s1 和元素和 s2 全部相同,那麼 s1 == s2; print({1, 2, 3} > {1, 2}) # True print({1, 2, 3} >= {1, 2, 3}) # True
以上就是集合支援的一些 API,還是很簡單的,下面來重點看一下集合的底層結構。
集合的資料結構定義在 setobject.h 中,那麼它長什麼樣子呢?
typedef struct { PyObject_HEAD Py_ssize_t fill; Py_ssize_t used; Py_ssize_t mask; setentry *table; Py_hash_t hash; Py_ssize_t finger; setentry smalltable[PySet_MINSIZE]; PyObject *weakreflist; } PySetObject;
解釋一下這些欄位的含義:
有了字典的經驗,再看集合會簡單很多。然後是 setentry,用於承載集合內的元素,那麼它的結構長什麼樣呢?相信你能夠猜到。
typedef struct { PyObject *key; Py_hash_t hash; } setentry;
相比字典少了一個 value,這是顯而易見的。因此集合的結構很清晰了,假設有一個集合 {3.14, "abc", 666},那麼它的結構如下:
由於集合裡面只有三個元素,所以它們都會存在 smalltable 陣列裡面,我們通過 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 = {3.14, "abc", 666} # 先來列印一下雜湊值 print('hash(3.14) =', hash(3.14)) print('hash("abc") =', hash("abc")) print('hash(666) =', hash(666)) """ hash(3.14) = 322818021289917443 hash("abc") = 8036038346376407734 hash(666) = 666 """ # 獲取PySetObject結構體範例 py_set_obj = PySetObject.from_address(id(s)) # 遍歷smalltable,列印索引、和雜湊值 for index, entry in enumerate(py_set_obj.smalltable): print(index, entry.hash) """ 0 0 1 0 2 666 3 322818021289917443 4 0 5 0 6 8036038346376407734 7 0 """
根據輸出的雜湊值我們可以斷定,這三個元素確實存在了 smalltable 陣列裡面,並且 666 存在了陣列索引為 2 的位置、3.14 存在了陣列索引為 3 的位置、"abc" 存在了陣列索引為 6 的位置。
當然,由於雜湊值是隨機的,所以每次執行之後列印的結果都可能不一樣,但是整數除外,它的雜湊值就是它本身。既然雜湊值不一樣,那麼每次對映出的索引也可能不同,但總之這三個元素是存在 smalltable 陣列裡面的。
然後我們再考察一下其它的欄位:
s = {3.14, "abc", 666} py_set_obj = PySetObject.from_address(id(s)) # 集合裡面有 3 個元素,所以 fill 和 used 都是 3 print(py_set_obj.fill) # 3 print(py_set_obj.used) # 3 # 將集合元素全部刪除 # 這裡不能用 s.clear(),原因一會兒說 for _ in range(len(s)): s.pop() # 我們知道雜湊表在刪除元素的時候是偽刪除 # 所以 fill 不變,但是 used 每次會減 1 print(py_set_obj.fill) # 3 print(py_set_obj.used) # 0
fill 成員維護的是 active 態的 entry 數量加上 dummy 態的 entry 數量,所以刪除元素時它的大小是不變的;但 used 成員的值每次會減 1,因為它維護的是 active 態的 entry 的數量。所以只要不涉及元素的刪除,那麼這兩者的大小是相等的。
然後我們上面說不能用 s.clear(),因為該方法表示清空集合,此時會重置為初始狀態,然後 fill 和 used 都會是 0,我們就觀察不到想要的現象了。
刪除集合所有元素之後,我們再往裡面新增元素,看看是什麼效果:
s = {3.14, "abc", 666} py_set_obj = PySetObject.from_address(id(s)) for _ in range(len(s)): s.pop() # 新增一個元素 s.add(0) print(py_set_obj.fill) # 3 print(py_set_obj.used) # 1
多次執行的話,會發現列印的結果可能是 3、1,也有可能是 4、1。至於原因,有了字典的經驗,相信你肯定能猜到。
首先新增元素之後,used 肯定為 1。至於 fill,如果新增元素的時候,正好撞上了一個 dummy 態的 entry,那麼將其替換掉,此時 fill 不變,仍然是 3;如果沒有撞上 dummy 態的 entry,而是新增在了新的位置,那麼 fill 就是 4。
for i in range(1, 10): s.add(i) print(py_set_obj.fill) # 10 print(py_set_obj.used) # 10 s.pop() print(py_set_obj.fill) # 10 print(py_set_obj.used) # 9
在之前程式碼的基礎上,繼續新增 9 個元素,然後 used 變成了10,這很好理解,因為此時集合有 10 個元素。但 fill 也是10,這是為什麼?很簡單,因為雜湊表擴容了,擴容時會刪除 dummy 態的 entry,所以 fill 和 used 是相等的。同理,如果再繼續 pop,那麼 fill 和 used 就又變得不相等了。
集合的結構我們已經清楚了,再來看看它的初始化過程。我們呼叫類 set,傳入一個可迭代物件,便可建立一個集合,這個過程是怎樣的呢?
PyObject * PySet_New(PyObject *iterable) { //底層呼叫了make_new_set return make_new_set(&PySet_Type, iterable); }
底層提供了PySet_New函數用於建立一個集合,接收一個可迭代物件,然後呼叫 make_new_set 進行建立。
static PyObject * make_new_set(PyTypeObject *type, PyObject *iterable) { // PySetObject *指標 PySetObject *so; // 申請集合所需要的記憶體 so = (PySetObject *)type->tp_alloc(type, 0); //申請失敗,返回 NULL if (so == NULL) return NULL; // fill 和 used 初始都為 0 so->fill = 0; so->used = 0; // PySet_MINSIZE 預設為 8 // 而 mask 等於雜湊表容量減 1,所以初始值是 7 so->mask = PySet_MINSIZE - 1; // 初始化的時候,setentry 陣列顯然是 smalltable // 所以讓 table 指向 smalltable 陣列 so->table = so->smalltable; // 初始 hash 值為 -1 so->hash = -1; // finger為0 so->finger = 0; // 弱參照列表為NULL so->weakreflist = NULL; //以上只是初始化,如果可迭代物件不為 NULL //那麼把元素依次設定到集合中 if (iterable != NULL) { //該過程是通過 set_update_internal 函數實現的 //該函數內部會遍歷 iterable,將迭代出的元素依次新增到集合裡面 if (set_update_internal(so, iterable)) { Py_DECREF(so); return NULL; } } //返回初始化完成的set return (PyObject *)so; }
整個過程沒什麼難度,非常好理解。
以上就是集合相關的內容,它的效率也是非常高的,能夠以O(1)的複雜度去查詢某個元素。最關鍵的是,它用起來也特別的方便。
此外 Python 裡面還有一個 frozenset,也就是不可變的集合。但 frozenset 物件和 set 物件都是同一個結構體,只有 PySetObject,沒有 PyFrozenSetObject。
我們在看 PySetObject的時候,發現該物件裡面也有個 hash 成員,如果是不可變集合,那麼 hash 值是不為 -1 的,因為它不可以新增、刪除元素,是不可變物件。
到此這篇關於淺析Python是如何實現集合的的文章就介紹到這了,更多相關Python集合內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45