這篇文章帶給大家的內容是關於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; }
a = [1, 2, 3] a.append(1)
0 <= ob_size <= allocated len(list) == ob_size
#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
當原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中文網其他相關文章!