首頁 > 後端開發 > Python教學 > Python列表的長度調節方法(附程式碼)

Python列表的長度調節方法(附程式碼)

不言
發布: 2018-12-12 10:24:06
轉載
9170 人瀏覽過

這篇文章帶給大家的內容是關於Python清單的長度調節方法(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

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

有像這樣使用:

>>> test = []
>>> test.append(1)
>>> test.append({2})
>>> test.append([3])
>>> print test

# 输出 
[1, set([2]), [3]]
登入後複製

也有像這樣使用的:

test = []

for i in range(4):
    test.append(i)
print test

# 输出 
[0, 1, 2, 3]
登入後複製

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

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

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

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

吝嗇的初始化

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

然後實際上list 真的就是這麼」low「:

import sys

test = []
test_1 = [1]
print sys.getsizeof(test)
print sys.getsizeof(test_1) - sys.getsizeof(test)

# 输出 
72     # 空列表内存大小,也是 list 对象的总大小
8       # 代表增加一个成员,list 增加的大小
登入後複製

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

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

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

PyObject *
PyList_New(Py_ssize_t size)
{
    PyListObject *op;
    size_t nbytes;

    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
    /* Check for overflow without an actual overflow,
     *  which can cause compiler to optimise out */
    if ((size_t)size > PY_SIZE_MAX / sizeof(PyObject *))
        return PyErr_NoMemory();
        
    // list对象指针的缓存
    if (numfree) {
        numfree--;
        op = free_list[numfree];
        _Py_NewReference((PyObject *)op);
    } else {
        op = PyObject_GC_New(PyListObject, &PyList_Type);
        if (op == NULL)
            return NULL;
    }
    
    // list 成员的内存申请
    nbytes = size * sizeof(PyObject *);
    if (size <= 0)
        op->ob_item = NULL;
    else {
        op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);
        if (op->ob_item == NULL) {
            Py_DECREF(op);
            return PyErr_NoMemory();
        }
        memset(op->ob_item, 0, nbytes);
    }
    Py_SIZE(op) = size;
    op->allocated = size;
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}
登入後複製

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

根據成員的數目,建立對應長度的空列表;(上述程式碼)

一個個將這些成員塞進去;

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

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

#define PyList_SET_ITEM(op, i, v) (((PyListObject *)(op))->ob_item[i] = (v))
登入後複製

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

可變長的關鍵

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

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

在 list 裡面,不管是 insert 、pop 還是 append,都會遇到 list_resize,故名思義,這個函數就是用來調整 list 物件的記憶體佔用的。

static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated;
    Py_ssize_t allocated = self->allocated;

    /* Bypass realloc() when a previous overallocation is large enough
       to accommodate the newsize.  If the newsize falls lower than half
       the allocated size, then proceed with the realloc() to shrink the list.
    */
    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SIZE(self) = newsize;
        return 0;
    }

    /* 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;
    }

    if (newsize == 0)
        new_allocated = 0;

    # 申请内存
    items = self->ob_item;
    if (new_allocated <= (PY_SIZE_MAX / sizeof(PyObject *)))
        PyMem_RESIZE(items, PyObject *, new_allocated);
    else
        items = NULL;
    if (items == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    self->ob_item = items;
    Py_SIZE(self) = newsize;
    self->allocated = new_allocated;
    return 0;
}
登入後複製
在上面的程式碼中,頻繁看到兩個名詞:newsize 和 new_allocated, 這裡需要解釋下,newsize 並不是 增加/減少 的個數,而是 增加/減少 之後的成員總數目。比方說:

a = [1, 2, 3]
a.append(1)
登入後複製
上面的 append 觸發list_resize 時, newsize 是 3 1, 而不是 1;這邊比較重要,因為在 pop 這類減少列表成員時候,就是傳入縮減後的總數目。

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

它們之間的關係就是:

 0 <= ob_size <= allocated
 len(list) == ob_size
登入後複製

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

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

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

#先確定一個基數:new_allocated = (newsize >> 3) (newsize < ; 9 ? 3 : 6);

判斷下new_allocated newsize 有沒有超過 PY_SIZE_MAX, 如果超過了,直接報錯;

最終確定新的總坑數是:new_allocated newsize, 如果newsizeize是0, 那麼總坑數直接為0 ;

下面示範下:

#coding: utf8
import sys

test = []
raw_size = sys.getsizeof(test)

test.append(1)
print "1 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)

test.append(1)
print "2 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)

test.append(1)
print "3 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)

test.append(1)
print "4 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)

test.append(1)
print "5 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)

test.append(1)
print "6 次 append 减去空列表的内存大小:%s " % (sys.getsizeof(test) - raw_size)
登入後複製
# 输出结果
1 次 append 减去空列表的内存大小:32
2 次 append 减去空列表的内存大小:32
3 次 append 减去空列表的内存大小:32
4 次 append 减去空列表的内存大小:32
5 次 append 减去空列表的内存大小:64
6 次 append 减去空列表的内存大小:64
登入後複製
開始簡單的代入法一步步算:

#其中:

new_allocated =  (newsize >> 3) (newsize < 9 ? 3 : 6) newsize (因為下面的newsize > 0)

當原allocated >= newsize 並且newsize >=  原

當原allocated >= newsize 並且newsize >=  原allocated  / 2 時,不改變 allocated 不申請記憶體直接回傳

第 n 次 append 列表原长度 新增成员数 原 allocated newsize new_allocated
1 0 1 0 0 + 1 = 1 3 + 1 = 4
2 1 1 4 1 + 1 = 2 无需改变
3 2 1 4 2 + 1 = 3 无需改变
4 3 1 4 3 + 1 = 4 无需改变
5 4 1 4 4 + 1 = 5 3 + 5 = 8
6 5 1 8 5 + 1 = 6 无需改变

通过上面的表格,应该比较清楚看到什么时候会触发改变 allocated,并且当触发时它们是如何计算的。为什么我们需要这样关注 allocated?理由很简单,因为这个值决定了整个 list 的动态内存的占用大小;

扩容是这样,缩容也是照猫画虎。反正都是算出新的 allocated, 然后由 PyMem_RESIZE 来处理。

总结

综上所述,在一些明确列表成员或者简单处理再塞入列表的情况下,我们不应该再用下面的方式:

test = []

for i in range(4):
    test.append(i)
print test
登入後複製

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

以上是Python列表的長度調節方法(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:segmentfault.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板