python學(xué)習(xí)筆記 -- list內(nèi)部實(shí)現(xiàn)(轉(zhuǎn))


看一下python的 cpython 實(shí)現(xiàn)(cpython就是python的c實(shí)現(xiàn)版本)

l = []
l.append(1)
l.append(2)
l.append(3)

列表對(duì)象的c語(yǔ)言結(jié)構(gòu)體


Cpython 中的列表實(shí)現(xiàn)類似于下面的 C 結(jié)構(gòu)體疮鲫。ob_item 是指向列表對(duì)象的指針數(shù)組八秃。allocated 是申請(qǐng)內(nèi)存的槽的個(gè)數(shù)塌碌。

列表初始化


看看初始化一個(gè)空列表的時(shí)候發(fā)生了什么薇正,例如:l = []。

arguments: size of the list = 0
returns: list object = []
PyListNew:
    nbytes = size * size of global Python object = 0
    allocate new list object
    allocate list of pointers (ob_item) of size nbytes = 0
    clear ob_item
    set list's allocated var to 0 = 0 slots
    return list object

要分清列表大小和分配的槽大小漓帅,這很重要埠况。列表的大小和 len(l) 的大小相同。分配槽的大小是指已經(jīng)在內(nèi)存中分配了的槽空間數(shù)昭殉。通常分配的槽的大小要大于列表大小苞七,這是為了避免每次列表添加元素的時(shí)候都調(diào)用分配內(nèi)存的函數(shù)。下面會(huì)具體介紹挪丢。

append操作


向列表添加一個(gè)整數(shù):l.append(1) 時(shí)發(fā)生了什么蹂风?調(diào)用了底層的 C 函數(shù) app1()。

arguments: list object, new element
returns: 0 if OK, -1 if not
app1:
    n = size of list
    call list_resize() to resize the list to size n+1 = 0 + 1 = 1
    list[n] = list[0] = new element
    return 0

下面是 list_resize() 函數(shù)乾蓬。它會(huì)多申請(qǐng)一些內(nèi)存惠啄,避免頻繁調(diào)用 list_resize() 函數(shù)。列表的增長(zhǎng)模式為:0任内,4撵渡,8,16死嗦,25趋距,35,46越除,58节腐,72,88……

python的這個(gè)值是怎么來(lái)的呢
So just checking very quickly, Ruby (1.9.1-p129) appears to use 1.5x when appending to an array, and Python (2.6.2) uses 1.125x plus a constant: (in Objects/listobject.c):
換個(gè)說(shuō)法摘盆,每當(dāng)來(lái)了一個(gè)新要求的大幸砣浮(比如插入操作中的原大小+1,或刪除操作中原大小-1):newsize,這時(shí)python并不直接對(duì)list的空間進(jìn)行調(diào)整骡澈。而是作個(gè)比較锅纺,若新要求的大小在總?cè)萘恐拢側(cè)萘康囊话胫蟿t肋殴,不進(jìn)行調(diào)整囤锉。

/* This over-allocates proportional to the list size, making room
* for additional growth.  The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
     PyErr_NoMemory();
     return -1;
} else {
     new_allocated += newsize;
}
arguments: list object, new size
returns: 0 if OK, -1 if not
list_resize:
    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) = 3
    new_allocated += newsize = 3 + 1 = 4
    resize ob_item (list of pointers) to size new_allocated
    return 0

現(xiàn)在分配了 4 個(gè)用來(lái)裝列表元素的槽空間坦弟,并且第一個(gè)空間中為整數(shù) 1。如下圖顯示 l[0] 指向我們新添加的整數(shù)對(duì)象官地。虛線的方框表示已經(jīng)分配但沒有使用的槽空間酿傍。

列表追加元素操作的平均復(fù)雜度為 O(1)。



繼續(xù)添加新的元素:l.append(2)驱入。調(diào)用 list_resize 函數(shù)赤炒,參數(shù)為 n+1 = 2, 但是因?yàn)橐呀?jīng)申請(qǐng)了 4 個(gè)槽空間亏较,所以不需要再申請(qǐng)內(nèi)存空間莺褒。再添加兩個(gè)整數(shù)的情況也是一樣的:l.append(3),l.append(4)雪情。下圖顯示了我們現(xiàn)在的情況遵岩。


insert操作


在列表偏移量 1 的位置插入新元素,整數(shù) 5:l.insert(1,5)巡通,內(nèi)部調(diào)用ins1() 函數(shù)尘执。

arguments: list object, where, new element
returns: 0 if OK, -1 if not
ins1:
    resize list to size n+1 = 5 -> 4 more slots will be allocated
    starting at the last element up to the offset where, right shift each element 
    set new element at offset where
    return 0

虛線的方框依舊表示已經(jīng)分配但沒有使用的槽空間。現(xiàn)在分配了 8 個(gè)槽空間宴凉,但是列表的大小卻只是 5誊锭。

列表插入操作的平均復(fù)雜度為 O(n)。

Pop 操作


取出列表最后一個(gè)元素 即l.pop()弥锄,調(diào)用了 listpop() 函數(shù)丧靡。在 listpop() 函數(shù)中會(huì)調(diào)用 list_resize 函數(shù),如果取出元素后列表的大小小于分配的槽空間數(shù)的一半籽暇,將會(huì)縮減列表的大小窘行。

arguments: list object
returns: element popped
listpop:
    if list empty:
        return null
    resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage
    set list object size to 4
    return last element

列表 pop 操作的平均復(fù)雜度為 O(1)。


可以看到 pop 操作后槽空間 4 依然指向原先的整數(shù)對(duì)象图仓,但是最為關(guān)鍵的是現(xiàn)在列表的大小已經(jīng)變?yōu)?4罐盔。

繼續(xù) pop 一個(gè)元素。在 list_resize() 函數(shù)中救崔,size – 1 = 4 – 1 = 3 已經(jīng)小于所分配的槽空間大小的一半惶看,所以縮減分配的槽空間為 6,同時(shí)現(xiàn)在列表的大小為 3六孵。

可以看到槽空間 3 和 4 依然指向原先的整數(shù)纬黎,但是現(xiàn)在列表的大小已經(jīng)變?yōu)?3。

再?gòu)目s小來(lái)看劫窒,當(dāng)newsize小于allocated/2時(shí)本今,意味著需要縮小空間大小了(節(jié)約內(nèi)存)。
該縮小多少呢,同樣是基于上面那個(gè)函數(shù)冠息。由它計(jì)算出一個(gè)增量來(lái)挪凑,在什么基礎(chǔ)上增呢?
allocated/2逛艰,對(duì)就是在這個(gè)基礎(chǔ)上躏碳,因?yàn)橐坏┯捎趧h除操作導(dǎo)致newsize恰好小于allocated/2時(shí),就會(huì)執(zhí)行縮小list空間大小的操作散怖。這樣菇绵,即節(jié)省了內(nèi)存,又不至于減少內(nèi)存過少镇眷,導(dǎo)致效率降低(想像一下咬最,如果每次小于allocated/2時(shí),就縮小為allocated/2欠动,那么如果對(duì)于那么刪除后立即執(zhí)行插入操作效率就很不理想了丹诀。)
以上這個(gè)策略,可以實(shí)現(xiàn)不會(huì)過去頻繁地調(diào)用realloc這個(gè)低效率的函數(shù)翁垂。

Remove 操作

Python 的列表對(duì)象有個(gè)方法,刪除指定的元素: l.remove(5)硝桩。底層調(diào)用 listremove() 函數(shù)沿猜。

arguments: list object, element to remove
returns none if OK, null if not
listremove:
    loop through each list element:
        if correct element:
            slice list between element's slot and element's slot + 1
            return none
    return null

為了做列表的切片并且刪除元素,調(diào)用了 list_ass_slice() 函數(shù)碗脊,它的實(shí)現(xiàn)方法比較有趣啼肩。我們?cè)趧h除列表位置 1 的元素 5 的時(shí)候,低位的偏移量為 1 同時(shí)高位的偏移量為 2.

arguments: list object, low offset, high offset
returns: 0 if OK
list_ass_slice:
    copy integer 5 to recycle list to dereference it
    shift elements from slot 2 to slot 1
    resize list to 5 slots
    return 0


another one


列表是python中簡(jiǎn)單而重要的數(shù)據(jù)結(jié)構(gòu)
list_sample = [1, 2, 3]

超預(yù)分配的量大概只有總量的八分之一衙伶,保證不太浪費(fèi)的情況下祈坠,也有線性的攤分復(fù)雜度。
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)

當(dāng)增加或刪除都有可能引起allocated的變化矢劲,當(dāng)目前的allocated滿足
allocated >= newsize && newsize >= (allocated >> 1)
這個(gè)關(guān)系時(shí)赦拘,allocated不變,不然更新分配值
allocated = new_allocated + newsize

由于python列表中的元素可以是任意的對(duì)象芬沉。
在底層實(shí)現(xiàn)上躺同,由于對(duì)象大小未知,并不能像數(shù)組那樣連續(xù)排在內(nèi)存里丸逸。
python列表維護(hù)了一個(gè)指針數(shù)組蹋艺,每個(gè)指針指向不同的對(duì)象,
這也造成了一些弊端黄刚,例如列表中對(duì)象大小一樣的時(shí)候就很虧了捎谨,浪費(fèi)空間不說(shuō),
跟C的數(shù)組相比,它離散的對(duì)象位置不能很好地利用CPU高速緩存涛救,造成了遍歷需要更多的CPU周期畏邢。
當(dāng)然也有優(yōu)點(diǎn),例如在某個(gè)位置insert一個(gè)新的元素時(shí)州叠,只要挪動(dòng)部分指針的值就OK了棵红。

一些操作的時(shí)間復(fù)雜度:
append:O(len(append_str))
insert:O(len(str) + len(insert_str))

tuple與list有什么區(qū)別?最重要的區(qū)別就是tuple是immutable咧栗,而list是mutable逆甜,
那么也就是說(shuō)tuple大小將不會(huì)改變,就不用像list那樣搞預(yù)分配了致板,更節(jié)省內(nèi)存交煞。

很多人說(shuō)tuple比list快,真的如此嗎斟或?
Python代碼

size = 1000000  

a = []  
for i in xrange(0, size):  
    a.append(i)  
b = tuple(a)  

for t in xrange(0, 32):  
    sum = 0  
    for e in b:  
        sum += e  

分別遍歷list和tuple素征,跑得的時(shí)間是6.925s和6.771s
從實(shí)測(cè)看來(lái),這個(gè)結(jié)論是不明顯的萝挤。

list和tuple在c實(shí)現(xiàn)上是很相似的御毅,對(duì)于元素?cái)?shù)量大的時(shí)候,
都是一個(gè)數(shù)組指針怜珍,指針指向相應(yīng)的對(duì)象端蛆,找不到tuple比list快的理由。
但對(duì)于小對(duì)象來(lái)說(shuō)酥泛,tuple會(huì)有一個(gè)對(duì)象池今豆,所以小的、重復(fù)的使用tuple還有益處的柔袁。

為什么要有tuple呆躲,還有很多的合理性。
實(shí)際情況中的確也有不少大小固定的列表結(jié)構(gòu)捶索,例如二維地理坐標(biāo)等插掂;
另外tuple也給元素天然地賦予了只讀屬性。

認(rèn)為tuple比list快的人大概是把python的tuple和list類比成C++中的數(shù)組和列表了腥例。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末燥筷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子院崇,更是在濱河造成了極大的恐慌肆氓,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件底瓣,死亡現(xiàn)場(chǎng)離奇詭異谢揪,居然都是意外死亡蕉陋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門拨扶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)凳鬓,“玉大人,你說(shuō)我怎么就攤上這事患民∷蹙伲” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵匹颤,是天一觀的道長(zhǎng)仅孩。 經(jīng)常有香客問我,道長(zhǎng)印蓖,這世上最難降的妖魔是什么辽慕? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮赦肃,結(jié)果婚禮上溅蛉,老公的妹妹穿的比我還像新娘。我一直安慰自己他宛,他們只是感情好船侧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厅各,像睡著了一般镜撩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讯检,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音卫旱,去河邊找鬼人灼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛顾翼,可吹牛的內(nèi)容都是我干的投放。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼适贸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼灸芳!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起拜姿,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤烙样,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蕊肥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谒获,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了批狱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裸准。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赔硫,靈堂內(nèi)的尸體忽然破棺而出炒俱,到底是詐尸還是另有隱情,我是刑警寧澤爪膊,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布权悟,位于F島的核電站,受9級(jí)特大地震影響惊完,放射性物質(zhì)發(fā)生泄漏僵芹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一小槐、第九天 我趴在偏房一處隱蔽的房頂上張望拇派。 院中可真熱鬧,春花似錦凿跳、人聲如沸件豌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)茧彤。三九已至,卻和暖如春疆栏,著一層夾襖步出監(jiān)牢的瞬間曾掂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工壁顶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留珠洗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓若专,卻偏偏與公主長(zhǎng)得像许蓖,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子调衰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容