python 對象
在python中,對象就是為C中的結構體在堆上申請的一塊內存,一般來說,對象是不能被靜態初始化的,並且不能再棧空間上生存。本文主要對Python的基本數據類型做簡單的介紹。
PyObject
在python中,所有東西都是對象,而所欲的對象都擁有一些共性(object.h/PyObject)。
PyObject是整個python對象機制的核心。
typedef struct _object {
PyObject_HEAD
} PyObject;
在release模式下編譯python時,PyObject如下:
typedef struct _object {
int ob_refcnt; //引用計數
struct _typeobject *ob_type; //類型
} PyObject;
其中,ob_refcnt為int整型,實現了基於引用計數的垃圾收集機制。對於某一對象A,當有一個新的PyObject引用該對象時,A的引用計數應該增加;而當這個PyObject被刪除時,A的引用計數應該減少;當A的引用計數減少到0時,A就可以從堆上被刪除,以釋放出內存供別的對象使用。
引用計數
PyObject中的ob_refcnt是一個32位的整形變量,這實際蘊含着Python所有的一個假設,既對一個對象的引用不會超過一個整形變量的最大值。
整數對象
小整數對象
在python中,所有對象都存活在堆上,python重復地使用malloc申請空間,大大降低了運行效率,造成大量內存碎片,影響整體性能。因此,在python中,對小整數對象使用了對象池技術,用來緩存所有的小整形對象(對重復的對象不需要重復的malloc)。
NSMALLPOSINTS和NSMALLNEGINTS來修改小整數對象池的范圍(默認分別為257和-5)。
大整數對象
對於小整數,使用對象池技術完全緩存其PyIntObject對象,而對於其他整數,python提供了一種PyIntBlock結構供大整形對象使用。PyIntBlock是通過維護一塊內存(Block)來供大整數使用,並通過單向列表block_list來維護。
Python的設計者為了提高代碼執行效率放棄了類型安全使用了宏來代替函數。
當一個PyIntObject對象被銷毀時,它所占用的內存並不會被釋放,而是繼續被python保留着,加入到free_list所維護的自有內存鏈表,為其他需要創建對象的內存使用。
字符串對象
在python中,PyStringObject是字符串對象的實現,它是一個擁有可變長度內存的對象。
trpdef struct {
PyObject_VAR_HEAD //ob_size字符串長度
long ob_shash; //字符串hash值
int ob_sstate;
char ob_sval[1];
} PyStringObject;
在PyStringObject中,還使用了intern機制和緩存池技術。
intern機制和緩存池
intern機制的目的:保證被intern之后的字符串在python整個運行期間只對應唯一的一個PyStringObject對象。
intern機制的關鍵是在系統中有一個(key,value)映射的集合,記錄所有被intern機制處理過的PyStringObject對象。當python在創建一個字符串時,會首先在interned中檢查是否已經有該字符串對應的PyStringObject對象了,如果有,則不用創建新的,這樣可以節省內存空間。其實,Python始終會為字符串創建PyStringObject對象,intern機制是在創建之后才會生效的,通常python在運行時創建一個PyStrhingObject對象temp后,基本就會銷毀該對象(引用計數減1)。
類似小整形的緩存池,python為PyStringObject中的一個字節的字符對應的PyStringObject對象設計對象池characters。
Python中的處理順序:
- 判斷是否為一個字符
- 創建PyStringObject對象進行intern操作
- 將intern結果緩存到字符緩沖池中
PyStringObject效率問題
在Python中"+"操作符進行字符串連接的方法效率極其低下,其根源在於python中的PyStringObject對象是一個不可變對象
。這就意味着當進行字符串連接時,實際上必須要創建一個新的PyStringObject對象。因此,如果需要連接N個PyStringObject對象,就必須進行N-1次內存申請及內存搬運工作,嚴重影響python的執行效率。
官方推薦利用PyStringObject對象的join操作來對存儲在list和tuple中一組PyStringObject對象進行連接操作,只需要分配一次內存,執行效率大大提高。
join操作會統計出list中所有PyStringObject對象的字符串長度,然后申請內存。
列表對象
PyListObject是Python提供的對列表的抽象。它類似於C++中的vector,而不是list。
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item; //元素首地址
int allocated; //實際申請內存的個數(部分沒有被使用,類似於vector)
} PyListObject;
其中,ob_item為指向元素列表的指針。
python所采用的內存管理策略和c++中vector采取的內存管理策略一樣,並不是寸多少數據就要申請對應大小的空間,這樣內存管理的效率較低,因此,在每一次申請內存時PyListObject總會申請一大塊內存,該內存的大小就記錄在allocated中,而實際被使用的內存數量記錄在ob_size中。
對象緩存池
在創建一個新的list時,首先創建PyListObject對象,然后創建PyListObject對象所維護的元素列表;在銷毀一個List時,首先銷毀PyListObject所維護的列表,然后釋放掉PyListObject本身,但是在釋放之前python會檢查緩沖池free_lists,查看其中緩存的PyListObject的數量是佛已經滿了,如果沒有,就將該數據對象加入到穿存池中,否則刪除。
Dict對象
與map不同,PyDictObject采用散列表(hash table)。在最優情況下,散列表能提供O(1)復雜度的搜索效率。
散列表
散列表的基本思想是通過一個函數將需搜索的鍵值映射為一個整數,將這個整數視為索引值訪問某片連續性的內存區域。
在使用散列表的過程中,不同的對象經過散列函數的作用,可能被映射為相同的散列值。隨着需要存儲的數據的增多,這樣的沖突就會發生得越來越頻繁。散列沖突是散列表不可避免的問題,當散列表的裝載率(已使用空間和總空間的比值)大於2/3時,散列沖突發生的概率就會大大增加。
在STL庫的hash table采用開鏈法來解決沖突,而python中采用開放地址法。當產生沖突時,python會通過一個二次探測函數f計算下一個候選位置,如果該候選位置可用,則可將待插入元素放到該位置,否則再次調用探測函數f,尋找可用位置。
關聯容器entry
typedef struct {
Py_ssize me_hash; //散列值
PyObject *me_key;
PyObject *me_value;
}
在PyDictObject對象生存變化的過程中,其中entry會在不同的狀態間轉換:Unused、Active和Dummy。
- Unused:當一個entry的me_key和me_value都是NULL;
- Active:當entry中存儲了一個(key,value)對時;
- dummy:當entry中存儲的(key,value)被刪除后,entry的狀態不能直接從Active轉到unused(偽刪除),否則會出現沖突探測鏈中斷。