一篇文章教你掌握python數(shù)據(jù)類型的底層實現(xiàn)
1. 列表
1.1 復制
淺拷貝
list_1 = [1, [22, 33, 44], (5, 6, 7), {"name":"Alina"}] list_3 = list_1 ## 錯誤?。≈皇菗Q了別名 list_2 = list_1.copy() ## 淺拷貝 ##或者 也可這樣實現(xiàn) ## list_1[:] ## list(list_1)
對拷貝前后兩個列表分別進行操作
list_2[1].append(55) print("list_1: ", list_1) print("list_2: ", list_2)
發(fā)現(xiàn)雖然淺拷貝了,但修改 list_2 的某些元素時,相應的 list_1 也有同樣的變化
1.2 列表的底層實現(xiàn) - 淺拷貝
通過 引用數(shù)組 實現(xiàn)列表元素的存儲
列表中存儲的并不是我們看到的元素的值,而是這些元素的地址
列表所謂的連續(xù),是在內(nèi)存中連續(xù)存儲元素的地址,而元素的值是在內(nèi)存中分散存儲的
當訪問到列表的某個元素時,是按照列表中存儲的元素地址去找到元素的值
直接賦值,是完完全全的沒改變原列表的任何內(nèi)容,就是原來的列表多了一個別名。
淺拷貝,確實是把列表拷貝了一份,也就是把列表中存儲的地址全部拷貝了一份給新列表,新列表擁有一份獨立的地址信息。但這些地址指向的元素和原列表是同一份元素。總結(jié),淺拷貝只是把地址重新拷貝了一份,他們指向的內(nèi)容還是同一份內(nèi)容。
1.3 淺拷貝 - 示例
1. 新增元素
新增元素時,list_1 列表中新存儲了一個指向元素100的地址,list_2 列表中新增了一個指向元素 ‘n' 的地址,因此互不影響。
2. 修改元素
當我們對 list_1[0]重新賦值的時候,實際是把這里原來存儲的指向元素1的地址,替換成了另一個地址——指向元素10的地址,下次我們?nèi)ist_1[0] 找元素的時候,會直接找到元素10,而不會再和原來的元素1有任何聯(lián)系。同樣的,list_2[0] 存放的地址換成了元素20的地址。
3. 列表型元素
list_1[1] 和 list_2[1] 存放了同一個地址列表,這個地址列表指向的也是同一批列表元素,所以修改 list_1[1]和list_2[1]的時候,都是對這批列表元素進行修改,是同時更新的。
4. 元組型元素
元組是不可變的?。?! 一旦改變了,就不再是這個元組了,而是一個新的元組。
所以要對元組執(zhí)行操作,都是先產(chǎn)生一個新元組,再在新元組上執(zhí)行相應操作。在這里就是先產(chǎn)生了一個新的地址元組(元組內(nèi)存儲了元素地址),再對新元組進行修改。
5. 字典型元素
對 list_1里的字典元素,增加一個鍵值對,發(fā)現(xiàn) list_2 里的字典元素也增加了鍵值對。
和列表型元素類似,在對列表型元素操作時,地址列表本身是不變的,我們對于地址列表的內(nèi)容進行操作。
在對字典型元素操作時,字典散列表本身也是不變的,我們對于字典散列表的內(nèi)容進行操作,按照新增的鍵找到對應位置,把新增的值存進去,這個新增值的存放位置,是由字典的鍵決定的。
6. 小結(jié)
列表,字典類型的元素,都是可變的,可以在地址不變的情況下改變內(nèi)容。
而元組,數(shù)字,字符串類型的元素,一旦內(nèi)容發(fā)生變化,那么地址也必須變化。
淺拷貝之后,針對不可變元素(元組,數(shù)字,字符串)的操作都生效了
針對可變元素(列表,字典)的操作,則發(fā)生了一些混淆。
當列表中出現(xiàn)了可變類型的元素,我們想對列表進行一個安全的復制,使得能夠獨立操作而不影響原列表,那么就不能淺拷貝,而是需要深拷貝。
1.4 列表的底層實現(xiàn) - 深拷貝
copy.deepcopy()
深拷貝將所有層級的相關元素全部完全的復制,避免了上述的混淆問題。
2. 字典
2.1 快速查找
慢 - 列表的查找
import time ls_1 = list(range(1000000)) ls_2 = list(range(500)) + [-10]*500 start = time.time() count = 0 for n in ls_2: if n in ls_1: count += 1 end = time.time() print("查找{}個元素,在ls_1中有{}個,共用時{}秒".format(len(ls_2), count, round(end-start)))) # 查找1000個元素,在ls_1中有500個,共用時6.19秒
快 - 字典的查找
import time d = {i:i for i in range(1000000)} ls_2 = list(range(500)) + [-10]*500 start = time.time() count = 0 for n in ls_2: try: d[n] except: pass else: count += 1 end = time.time() print("查找{}個元素,在ls_1中有{}個,共用時{}秒".format(len(ls_2), count, round(end-start))) # 查找1000個元素,在ls_1中有500個,共用時0秒
2.2 字典的底層實現(xiàn)
通過稀疏數(shù)組 實現(xiàn)值的存儲與訪問
1. 字典的創(chuàng)建過程
1.創(chuàng)建一個散列表(稀疏數(shù)組,N >>n,可以動態(tài)擴充)
2.通過hash()計算鍵的散列值
3.根據(jù)計算的散列值確定其在散列表中的位置(個別時候有哈希沖突,解決辦法是開放尋址法 或 鏈接法 )
4.在該位置上存入值
d = {} # d = dict() print(hash("python")) print(hash(1024)) print(hash(1.2)) # -477104656440599764... # 1024 # 3713081631934410656... d["age"] = 18 #增加鍵值對之前,首先計算鍵的散列值hash("age") print(hash("age")) #
2. 字典的訪問過程
1.計算要訪問的鍵的散列值
2.根據(jù)計算的散列值,按照一定的規(guī)則,確定其在散列表中的位置
3.讀取該位置上存儲的值(存在則返回該值,不存在則報錯 KeyError)
d["age"] #訪問鍵值對之前,首先計算鍵的散列值hash("age")
2.3 小結(jié)
字典數(shù)據(jù)類型,以空間換時間,內(nèi)存占用大,空間利用率低,但查找速度快(稀疏數(shù)組 N >> n,否則會產(chǎn)生很多沖突,另外動態(tài)擴充也是)因為鍵在字典中顯示的順序,與實際計算出來的它在散列表中的存放位置,是兩碼事,因此字典表現(xiàn)為無序的
之前專門寫過 —— Python字典及底層哈希
3. 字符串
通過緊湊數(shù)組 實現(xiàn)字符串的存儲
字符串數(shù)據(jù)在內(nèi)存中是連續(xù)存放的,空間利用率高
原因是:每個字符的大小是固定的,因此一個字符串的大小也是固定的,可以分配一個固定大小的空間給字符串。
同為序列類型,為什么列表采用引用數(shù)組,而字符串采用緊湊數(shù)據(jù)
雖然同為序列類型,但列表可以存儲的元素類型是多種多樣的,并且列表是可變的,無法預估內(nèi)存空間,所以列表不能通過緊湊數(shù)組。
4. 是否可變
不可變類型:數(shù)字,字符串,元組
(元組并不總是不可變的,元組內(nèi)存儲的元素也必須同時是不可變類型,否則該元組屬于可變)
在生命周期內(nèi)保持內(nèi)容不變,一旦內(nèi)容變了,就不再是它了( id / 地址也變了)
不可變對象的 += 擴充操作,實際上是創(chuàng)建了一個新的對象。
x = 1 print("x id:", id(x)) # x id: 1407184... x += 2 print("x id:", id(x)) # x id: 204099...
可變類型:列表,字典,集合
id (地址)不變的情況下,里面的內(nèi)容可以改變
可變對象的 += 操作,實際是在原對象的基礎上直接修改
ls = [1,2,3] print("ls id:", id(ls)) # ls id: 2040991750856 ls += [4,5] print("ls id:", id(ls)) # ls id: 2040991750856
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注本站的更多內(nèi)容!
版權聲明:本站文章來源標注為YINGSOO的內(nèi)容版權均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務器上建立鏡像,否則將依法追究法律責任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學習參考,不代表本站立場,如有內(nèi)容涉嫌侵權,請聯(lián)系alex-e#qq.com處理。