欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python里的dict和set的背后小秘密

 更新時間:2021年11月03日 15:32:29   作者:Python探索牛  
這篇文章主要介紹了在Python里的dict和set的背后小秘密,dict字典是Python中的重要基礎(chǔ)知識,set與其類似,需要的朋友可以參考下
  • Python里的dict和set的效率有多高?
  • 為什么它們是無序的?
  • 為什么并不是所有的Python對象都可以當(dāng)作dict的鍵或set里的元素?
  • 為什么dict的鍵和set的元素的順序是根據(jù)它們被添加的次序而定的,以及為什么在映射對象的生命周期中,這個順序并不是一成不變的?
  • 為什么不應(yīng)該在迭代循環(huán)dict或是set的同時往里添加元素?

Python里的dict和set的效率有多高?

由實驗得知,不管查詢有多少個元素的字典或集合,所耗費的時間都能忽略不計(前提是字典或者集合不超過內(nèi)存大小).

字典中的散列表

散列表其實是一個稀疏數(shù)組(總是有空白元素的數(shù)組被稱為稀疏數(shù)組).在一般的數(shù)據(jù)結(jié)構(gòu)教材中,散列表里的單元通常叫作表元(bucket).

在dict的散列表當(dāng)中,每個鍵值對都占用一個表元,每個表元都有兩個部分,一個是對鍵的引用,另一個是對值的引用.

因為所有的表元的大小一致,所以可以通過偏移量來讀取某個表元.

Python會設(shè)法保證大概還有三分之一的表元是空的,所以在快要達(dá)到這個閾值的時候,原有散列表會被復(fù)制到一個更大的空間里面.

如果要把一個對象放入散列表,那么首先要計算這個元素鍵的散列值.Python中可以用hash()方法來做這件事情.

1.散列值和相等性

內(nèi)置的hash()方法可以用于所有的內(nèi)置類型對象.如果是自定義對象調(diào)用hash()的話,實際上運行的是自定義的__hash__.

如果這兩個對象在比較的時候是相等的,那么它們的散列值必須相等,否則散列表就不能正常運行了.

例如,如果11.0為真,那么hash(1)hash(1.0)也必須為真,但其實這兩個數(shù)字(整型和浮點)的內(nèi)部結(jié)構(gòu)是完全不一樣的.

既然提到了整型,CPython的實現(xiàn)細(xì)節(jié)里有一條是:如果有一個整型對象,而且它能被存進(jìn)一個機器字中,那么它的散列值就是它本身的值.

為了讓散列值能夠勝任散列表索引這一角色,它們必須在索引空間中盡量分散開來.這意味著在最理想的狀況下,越是相似但不相等的對象,它們散列值的差別應(yīng)該越大.

"""
import sys
# 通過sys.maxsize獲取操作系統(tǒng)的整數(shù)最大值,轉(zhuǎn)換成二進(jìn)制,計算位數(shù),加上一個符號位
MAX_BITS = len(format(sys.maxsize, 'b'))
print('%s-bit Python build' % (MAX_BITS + 1))
def hash_diff(o1, o2):
    h1 = '{:>0{}b}'.format(hash(o1), MAX_BITS)  # 計算o1的散列值,并用0補滿空位
    h2 = '{:>0{}b}'.format(hash(o2), MAX_BITS)  # 計算o2的散列值,并用0補滿空位
    # 比較h1和h2的每一位,用!標(biāo)識出來,否則用' '表示
    diff = ''.join('!' if b1 != b2 else ' ' for b1, b2 in zip(h1, h2))
    count = '!={}'.format(diff.count('!'))  # 顯示不同的總數(shù)
    width = max(len(repr(o1)), len(repr(o2)), 8)  # 行頭的寬度
    sep = '_' * (width * 2 + MAX_BITS)  # 分割線
    return '{!r:{width}} {}\n{:{width}} {} {}\n{!r:{width}} {}\n{}'.format(
        o1, h1, ' ' * width, diff, count, o2, h2, sep, width=width
    )
print(hash_diff(1, 1.0))
print(hash_diff(1.0, 1.0001))
print(hash_diff(1.0001, 1.0002))
print(hash_diff(1.0002, 1.0003))

從Python3.3開始,str,bytes和datetime對象的散列值計算過程中多了隨機的'加鹽'這一步.

所加鹽值是Python進(jìn)程內(nèi)的一個常量,但是每次啟動Python解釋器都會生成一個不同的鹽值.

隨機鹽值的加入是為了防止DOS攻擊而采取的一種安全措施.

散列表算法

為了獲取my_dict[search_key]背后的值,Python首先會調(diào)用hash(search_key)來計算search_key的散列值,把這個值最低的幾位數(shù)字當(dāng)作偏移量,在散列表里查找表元(具體取幾位,得看當(dāng)前散列表的大小).若找到的表元是空的,則拋出KeyError異常.

若不是空的,則表元里會有一對found_key:found_value.這時候Python會檢驗search_key == found_key是否為真,如果是,就會返回found_value.

如果search_key和found_key不匹配的話,這種情況稱為[散列沖突].發(fā)生這種情況是因為,散列表所做的其實是把隨機的元素映射到只有幾位的數(shù)字上,而散列表本身的索引又只能依賴于這個數(shù)字的一部分.為了解決散列沖突,算法會在散列值中另外再取幾位,然后用特殊的方法處理一下,把新得到的數(shù)字再當(dāng)作索引來尋找表元.

若這次找到的表元是空的,則同樣拋出KeyError;若非空,或者鍵匹配,則返回這個值;或者又發(fā)現(xiàn)了散列沖突,則重復(fù)以上的步驟.

從字典中取值的算法流程如下:給定一個鍵,這個算法要么返回一個值,要么拋出KeyError異常

|-------------------------------------------------------------------------|
|計算鍵的散列值               ________使用散列值的另一部分來定位散列表中的零一行 |
|     |                    |                        ↑                     |
|     |                    |                        | 否 (散列沖突)        |
|     |                    ↓                        |                     |
|使用散列值的一部分         表元                       |                     |
|來定位散列表中的一 ------→ 為空? ---------否-------→ 鍵相等?                 |
|個表元                     |                        |                     |
|                          |是                       |是                   |
|                          ↓                         ↓                     |
|                    拋出KeyError                返回表元里的值              |
|--------------------------------------------------------------------------|

添加新元素和更新現(xiàn)有鍵值的操作幾乎跟上面一樣.只不過對于前者,在發(fā)現(xiàn)空表元的時候會放入一個新元素;

對于后者,在找到對應(yīng)的表元后,原表里值對象會被替換成新值.

另外在插入新值時,Python可能會按照散列表的擁擠程度來決定是否要重新分配內(nèi)存來為它擴容.如果增加了散列表的大小,那散列值所占的位數(shù)和用作索引的位數(shù)就會隨之增加,這樣做的目的是為了減少發(fā)生散列沖突的概率.

表面上看,這個算法似乎很費事,而實際上就是dict里有數(shù)百萬個元素,多數(shù)的搜索過程中并不會有沖突發(fā)生,平均下來每次搜索可能會有一到兩次沖突.

在正常情況下,就算是最不走運的鍵所遇到的沖突的次數(shù)用一只手也能數(shù)過來.

dict的實現(xiàn)及其導(dǎo)致的結(jié)果

1.鍵必須死可散列的

一個可散列的對象必須滿足以下要求:

1)支持hash()函數(shù),并且通過__hash__()方法所得到的散列值是不變的.

2)支持通過__eq__()方法來檢測相等性.

3)若a == b為真,則hash(a) == hash(b)也為真

所有由用戶定義的對象默認(rèn)都是可散列的,因為它們散列值由id()來獲取,而且它們都是不相等的.

如果你實現(xiàn)了一個類的__eq__()方法,并且希望它是可散列的,那么它一點要有個恰當(dāng)?shù)腳_hash__方法,保證a==b為真的情況下hash(a)==hash(b)也必定為真.

否則就會破壞恒定的散列表算法,導(dǎo)致由這些對象所組成的字典和集合完全失去可靠性,這個后果是非??膳碌?

另一方面,如果一個含有自定義__eq__依賴的類處于可變的狀態(tài),那就不要在這個類中實現(xiàn)__hash__方法,因為它的實例時不可散列的.

'''
學(xué)習(xí)中遇到問題沒人解答?小編創(chuàng)建了一個Python學(xué)習(xí)交流群:725638078
尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學(xué)習(xí)教程和PDF電子書!
'''
class A:
    def __init__(self, a):
        self.a = a
    def __hash__(self):
        return 1
    def __eq__(self, other):
        return hash_diff(self, other)
    def __repr__(self):
        return str(self.a)
a = A(1)
b = A(2)
d1 = {a: 1, b: 2, 1: 3}
print(d1)  # {1: 3}  會發(fā)現(xiàn)里面只有一個鍵值對

2.字典在內(nèi)存上的開銷巨大

由于字典使用了散列表,而散列表又必須時稀疏的,這導(dǎo)致它在空間上的效率低下.舉例而言.如果你需要存放數(shù)量巨大的記錄,那么放在由元組或是具名元組構(gòu)成的列表中會是比較好的選擇;

最好不要根據(jù)JSON的風(fēng)格,用由字典組成的列表來存放這些記錄,用元組取代字典能節(jié)省空間的原因有兩個:

其一是避免了散列表所消耗的空間. 其二是無需把記錄中字段的名字在每個元素里都存一遍.

在用戶自定義的類型中,__slots__屬性可以改變實例屬性的存儲方式,由dict變成tuple.

3.鍵查詢很快

dict的實現(xiàn)是典型的空間換時間:字典類型有著巨大的內(nèi)存開銷,但它們提供了無視數(shù)據(jù)量的快速訪問--只要字典能被裝在內(nèi)存里.

4.鍵的次序取決于添加順序

當(dāng)往dict里添加新鍵而又發(fā)生散列沖突的時候,新鍵可能會被安排存放到另一個位置.于是下面的這種情況就會發(fā)生:

由dict([(key1, value1), (key2, value2)])和dict([(key2, value2), (key1, value1)])得到的兩個字典,在進(jìn)行比較的時候,它們是相等的.

但是如果在key1和key2被添加到字典里的過程中有沖突發(fā)生的話,這兩個鍵出現(xiàn)在字典里的順序是不一樣的.

下面的示例展示了這個現(xiàn)橡.這個示例用同樣的數(shù)據(jù)創(chuàng)建了3個字典,唯一的區(qū)別就是數(shù)據(jù)出現(xiàn)的順序不一樣.可以看到,雖然鍵的次序是亂的,這3個字典仍然被視作相等的.

STUDENTS = [
    (89, '孫悟空'),
    (79, '豬八戒'),
    (69, '沙和尚'),
    (59, '小白龍'),
    (49, '唐僧')
]
d1 = dict(STUDENTS)
print('d1:', d1.keys())
d2 = dict(sorted(STUDENTS))
print('d2:', d2.keys())
d3 = dict(sorted(STUDENTS, key=lambda x: x[1]))
print('d3', d3.keys())
assert d1 == d2 and d2 == d3

5.往字典里添加新鍵可能會改變已有鍵的順序

無論何時往字典里添加新的鍵,Python解釋器都可能做出為字典擴容的決定.擴容導(dǎo)致的結(jié)果就是要新建一個更大的散列表,并把字典里已有的元素添加到新表里.

這個過程可能會發(fā)生新的散列沖突,導(dǎo)致新散列表中鍵的次序變化.

要注意的是,上面提到的這些變化是否會發(fā)生以及如何發(fā)生,都依賴于字典背后的實現(xiàn),因此你不能很自信的說自己知道背后發(fā)生了什么.

如果你在迭代一個字典的所有鍵的過程中同時對字典進(jìn)行修改,那么這個循環(huán)很可能會跳過一些鍵----甚至是跳過那些字典中已經(jīng)有的鍵.

由此可知,不要對字典同時進(jìn)行迭代和修改.如果想掃描并修改一個字典,最好分成兩步來進(jìn)行:

首先對字典迭代,以得出需要添加的內(nèi)容,把這些內(nèi)容放在一個新字典里;迭代結(jié)束之后再對原字典進(jìn)行更新.

在Python3中,.keys() .items() .values()方法返回的都是字典視圖.也就是說,這些方法返回的值更像集合.

set的實現(xiàn)及其導(dǎo)致的結(jié)果

set和frozenset的實現(xiàn)也依賴散列表,但在它們的散列表里存放的只有元素的引用.在set加入到Python之前,我們都是把字典加上無意義的值當(dāng)作集合來用.

1.集合里的元素必須是可散列的.

2.集合很消耗內(nèi)存.

3.可以很高效的判斷元素是否存在于某個集合.

4.元素的次序取決于被添加到集合里的次序.

5.往集合里添加元素,可能會改變集合里已有元素的次序.

總結(jié)

本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!

相關(guān)文章

  • Python 中 Elias Delta 編碼詳情

    Python 中 Elias Delta 編碼詳情

    這篇文章主要介紹了Python 中 Elias Delta 編碼,下面的文章我們將使用 python 實現(xiàn) Elias Delta 編碼。具體詳細(xì)內(nèi)容,需要的朋友可以參考一下
    2021-11-11
  • Python 解析XML文件

    Python 解析XML文件

    google一篇關(guān)于Python解析XML文件的博文不過XML文件出錯,整理如下。
    2009-04-04
  • Python實現(xiàn)統(tǒng)計英文文章詞頻的方法分析

    Python實現(xiàn)統(tǒng)計英文文章詞頻的方法分析

    這篇文章主要介紹了Python實現(xiàn)統(tǒng)計英文文章詞頻的方法,結(jié)合實例形式分析了Python針對英文單詞頻率統(tǒng)計的相關(guān)原理、實現(xiàn)方法及具體操作技巧,需要的朋友可以參考下
    2019-01-01
  • 如何使用 Python 中的功能和庫創(chuàng)建 n-gram的過程

    如何使用 Python 中的功能和庫創(chuàng)建 n-gram的過程

    在計算語言學(xué)中,n-gram 對于語言處理、上下文和語義分析非常重要,本文將討論如何使用 Python 中的功能和庫創(chuàng)建 n-gram,感興趣的朋友一起看看吧
    2023-09-09
  • Python+Selenium實現(xiàn)表單自動填充和提交

    Python+Selenium實現(xiàn)表單自動填充和提交

    你是不是也厭倦了每天重復(fù)表單填寫的工作,是時候讓技術(shù)來幫助我們解放雙手了,下面小編就為大家介紹一下如何使用Selenium和Python來自動填充和提交表單
    2023-09-09
  • keras實現(xiàn)VGG16方式(預(yù)測一張圖片)

    keras實現(xiàn)VGG16方式(預(yù)測一張圖片)

    這篇文章主要介紹了keras實現(xiàn)VGG16方式(預(yù)測一張圖片),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-07-07
  • python切片及sys.argv[]用法詳解

    python切片及sys.argv[]用法詳解

    Sys.argv[]是用來獲取命令行參數(shù)的,sys.argv[0]表示代碼本身文件路徑,所以參數(shù)從1開始。下面通過實例代碼給大家介紹python切片及sys.argv[]用法,需要的朋友參考下吧
    2018-05-05
  • 詳解pyqt5 動畫在QThread線程中無法運行問題

    詳解pyqt5 動畫在QThread線程中無法運行問題

    這篇文章主要介紹了詳解pyqt5 動畫在QThread線程中無法運行問題,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05
  • python一鍵升級所有pip package的方法

    python一鍵升級所有pip package的方法

    下面小編就為大家?guī)硪黄猵ython一鍵升級所有pip package的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01
  • Python安裝Bs4及使用方法

    Python安裝Bs4及使用方法

    這篇文章主要介紹了Python安裝Bs4及使用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04

最新評論