Python:淺析列表的變長變短

前言

Python 的列表(list)是一個非常靈活的數組,可以隨意調整長度。正是因為這種便利,使得我們會情不自禁地去修改數組以滿足我們的需求,其中相比於 insert,pop等等而言,append用法更常見。

有像這樣使用:

Python:淺析列表的變長變短

也有像這樣使用的:

Python:淺析列表的變長變短

這樣用很開心,也很滿足。

但其實只要遇到能夠動態修改數據長度場景,我們都應該馬上反應過來一點,那就是內存管理的問題。

如果運行效率和便捷性同時滿足的話,那簡直就是大大的福音呀。

然而,上帝為你開啟一扇窗的同時肯定也已經關上了一扇門了!

吝嗇的初始化

深受預分配知識的薰陶,我們也是覺得 list 在初始化是有分配一定的長度的,要不然每次都申請內存那得多 ”low“ 啊。

然後實際上 list 真的就是這麼 ”low“:

Python:淺析列表的變長變短

我們的猜測是,list 在定義之後,會預先分配好一個一定大小的池用來塞數據,以避免動不動就申請內存。

但是在上面的實驗看出,一個成員的列表,比一個空列表,長度僅僅只是大了 8 字節,如果真的存在這樣一個預分配的池,那麼在預分配個數之內添加成員,兩者的內存大小應該是保持不變才對。

所以可以猜測這塊list應該是沒有這樣的一個預分配內存池。這裡需要來個實錘


Python:淺析列表的變長變短

當我們在執行test = [1]時,實際上只做了兩件事:

  1. 根據成員的數目,構建相應長度的空列表;(上述代碼)
  2. 一個個將這些成員塞進去;

可能有童鞋會覺得,在塞成員的那一步,說不定會觸發什麼機制使它變大?

很可惜,因為初始化用的方法是PyList_SET_ITEM, 所以這裡是木有的觸發什麼機制,只是簡單的數組成員賦值而已:

Python:淺析列表的變長變短

所以整個 list 的初始化,還真的就是木有預分配的內存池,直接按需申請,一個蘿蔔一個坑,實在得狠;

可變長的關鍵

初始化過程是這樣還可以理解,如果運行中還這樣的話,那就有點說不過去了。

試想下,在文章開頭用append的例子中,如果每append一個元素就申請一次內存,那麼list可能要被吐槽到懷疑人生了, 所以很明顯,在對於內存的申請,它還是有自己的套路的。

在list裡面,不管是insert、pop還是append,都會遇到list_resize,故名思義,這個函數就是用來調整 list 對象的內存佔用的。

Python:淺析列表的變長變短

在上面的代碼中,頻繁看到兩個名詞:newsize 和 new_allocated, 這裡需要解釋下,newsize 並不是增加/減少的個數,而是增加/減少之後的成員總數目。比方說:


Python:淺析列表的變長變短

上面的 append 觸發 list_resize 時,newsize 是 3 + 1, 而不是 1;這邊比較重要,因為在pop 這類減少列表成員時候,就是傳入縮減後的總數目。

在 list 的結構定義中,關於長度的定義有兩個,分別是 ob_size (實際的成員數),allocated (總成員數)

它們之間的關係就是:

Python:淺析列表的變長變短

所以 new_allocated 就很好理解了,這個就是新的總坑數。

當名詞含義理解得差不多時,我們就能順藤摸瓜知道一個列表在 list_resize 之後,大小會變成怎樣?

方法其實從上面註釋和代碼都說得很明白了,這裡再簡單整理下:

  1. 先確定一個基數:new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
  2. 判斷下 new_allocated + newsize 有沒有超過 PY_SIZE_MAX, 如果超過了,直接報錯;
  3. 最終確定新的總坑數是:new_allocated + newsize, 如果 newsize 是 0, 那麼總坑數直接為 0 ;

下面演示下

Python:淺析列表的變長變短

Python:淺析列表的變長變短


開始簡單的代入法一步步算:

其中:

  • new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize (因為下面的 newsize > 0)
  • 當原 allocated >= newsize 並且 newsize >= 原 allocated / 2 時,不改變 allocated 不申請內存直接返回


Python:淺析列表的變長變短


通過上面的表格,應該比較清楚看到什麼時候會觸發改變 allocated,並且當觸發時它們是如何計算的。為什麼我們需要這樣關注 allocated?理由很簡單,因為這個值決定了整個 list 的動態內存的佔用大小;

擴容是這樣,縮容也是照貓畫虎。反正都是算出新的 allocated, 然後由PyMem_RESIZE來處理。

總結

綜上所述,在一些明確列表成員或者簡單處理再塞入列表的情況下,我們不應該再用下面的方式:

Python:淺析列表的變長變短

而是應該用更加 pythonic 和 更加高效的列表推導式:test = [i for i in range(4)]。

經過作者授權轉載|原文鏈接:https://segmentfault.com/a/1190000017221754

Python零基礎和Docker+K8s課程優惠,需要的諮詢wechat:17812796384


分享到:


相關文章: