Python——物件操作的記憶體分析
文章目錄
一、物件的本質
Python 中,一切皆物件。每個物件由:標識(identity)、型別(type)、value(值)
組成。
1)標識:用於唯一標識物件,通常對應於物件在計算機記憶體中的地址。使用內建函式id(obj)
可返回物件obj 的標識。
2)型別:用於表示物件儲存的“資料”的型別。型別可以限制物件的取值範圍以及可執行的
操作。可以使用type(obj)獲得物件的所屬型別。
3) 值:表示物件所儲存的資料的資訊。使用print(obj)可以直接打印出值。
物件的本質就是:一個記憶體塊(堆),擁有特定的值,支援特定型別的相關操作。
二、變數
1、變數的本質
在Python 中,變數也成為:物件的引用。因為,變數儲存的就是物件的地址。
變數通過地址引用了“物件”。
變數位於:棧記憶體。
物件位於:堆記憶體。
一個簡單的例子
>>> a = "Hello World"
>>> id(a)
1628889058416
在Python中,變數不需要顯式宣告型別。因為PVM會根據變數引用的物件,自動確定資料型別。
所以Python是動態語言。
2、變數的作用域
變數起作用的範圍稱為變數的作用域(Scope),不同作用域內同名變數之間互不影響。
變數分為:全域性變數、區域性變數。
全域性變數:
1)在函式和類定義之外宣告的變數。作用域為定義的模組,從定義位置開始直到模組
結束。
2)全域性變數降低了函式的通用性和可讀性。應儘量避免全域性變數的使用。
3)全域性變數一般做常量使用。
4)函式內要改變全域性變數的值,使用global 宣告一下
區域性變數:
1)在函式體中(包含形式引數)宣告的變數。
2)區域性變數的引用比全域性變數快,優先考慮使用。
3)區域性變數優先於全域性變數的使用(如果同名)。
區域性變數的查詢和訪問速度比全域性變數快,優先考慮使用,尤其是在迴圈的時候。
在特別強調效率的地方或者迴圈次數較多的地方,可以通過將全域性變數轉為區域性變數提高執行速度。
三、序列的記憶體分析
序列是一種資料儲存方式,用來儲存一系列的資料。在記憶體中,序列就是一塊用來存放
多個值的連續的記憶體空間。
序列中儲存的是物件的地址,而不是物件的值。
Python中常見的序列結構有:字串、列表、元組、字典、集合。
1、字串
字串快取池
Python 支援字串快取池機制,對於符合識別符號規則的字串(僅包含下劃線(_)、字母和數字)會啟用字串駐留機制駐留機制。
字串的比較
我們可以直接使用 == 或 != 對字串進行比較,是否含有相同的字元。
我們使用 is 或 not is,判斷兩個物件是否同一個物件。比較的是物件的地址,即 id(obj1) 是否和 id(obj2) 相等。
一個例子
>>> a = "abd_33"
>>> b = "abd_33"
>>> a is b
True
>>> c = "dd#"
>>> d = "dd#"
>>> c is d
False
>>> str1 = "aa"
>>> str2 = "bb"
>>> str1+str2 is "aabb"
False
>>> str1+str2 == "aabb"
True
2、列表
列表是內建可變序列,是包含多個元素的有序連續的記憶體空間。
>>> l = ['a', 'b', 'c', 'd']
>>> id(l)
1628897973960
>>> for x in l:
print(id(x))
1628888690168
1628888688488
1628887945320
1628887946776
3、字典:底層原理和記憶體分析
字典物件的核心是散列表。
散列表是一個稀疏陣列(總是有空白元素的陣列),陣列的每個單元叫做bucket。每個bucket 有兩部分:一個是鍵物件的引用,一個是值物件的引用。
由於,所有bucket 結構和大小一致,我們可以通過偏移量來讀取指定bucket。
一個鍵值對建立的過程
我們知道 dict 的底層是陣列(列表)。
那麼如何將一個鍵值對放進字典的問題,可以轉化為如何將一個 key 對映為 陣列的下標。
>>> d = {} # 建立一個空字典,我們假設它底層陣列的長度為 8
>>> name = 'Ez' # 想將 name = 'Ez' 新增到字典 t 中
>>> bin(hash(name))
'-0b111110111010011110001000100110110010001001110000110011110010010'
考慮到陣列的長度為8,
下標(十進位制) | 二進位制 |
---|---|
0 | 000 |
1 | 001 |
2 | 010 |
3 | 011 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
則我們從 bin(hash(name))
中取出最右的3位,即010,對應的下標為2。
檢測陣列的偏移量2(即下標為2的位置),對應的bucket 是否為空。如果為空,則將鍵值對放進去。如果不為空,則依次取右邊3 位作為偏移量,直到成功新增。
當散列表使用程度接近 2/3 時,陣列就會擴容:創造更大的陣列,將原有內容拷貝到新陣列中。
檢索一個鍵值對的過程
和儲存的底層流程演算法一致,也是依次取雜湊值的不同位置的數字。
先計算鍵物件的雜湊值,根據陣列長度確定每次取多少位雜湊值。
不為空,則將這個bucket 的鍵物件計算對應雜湊值,和我們的雜湊值進行比較,如果相等。則將對應“值物件”返回。如果如果不相等,則再依次取其他幾位數字,重新計算偏移量。依次取完後,仍然沒有找到。則返回None。
4、集合
集合是無序可變,元素不能重複。
實際上,集合底層是字典實現,集合的所有元素都是字典中的“鍵物件”,因此是不能重複的且唯一的。
四、函式的記憶體分析
函式也是物件(廢話,在Python中,一切皆物件)。
1、函式的引數傳遞
函式的引數傳遞本質上就是:從實參到形參的賦值操作。
Python 中“一切皆物件”,所有的賦值操作都是“引用的賦值”。所以,Python 中引數的傳遞都是“引用傳遞”,不是“值傳遞”。
具體操作時分為兩類:
1)對“可變物件”進行“寫操作”,直接作用於原物件本身。
2) 對“不可變物件”進行“寫操作”,會產生一個新的“物件空間”,並用新的值填
充這塊空間。(起到其他語言的“值傳遞”效果,但不是“值傳遞”)
可變物件有:字典、列表、集合、自定義的物件等。
不可變物件有:數字、字串、元組、function 等。
可變物件
傳遞引數是可變物件,實際傳遞的還是物件的引用。在函式體中不建立新的物件拷貝,而是可以直接修改所傳遞的物件。
b = [10,20]
def f1(m):
print("m的id:",id(m)) # b 和 m 是同一個物件
m.append(30) # 由於 m 是可變物件,不建立物件拷貝,直接修改這個物件
f1(b)
print("b的id:",id(b))
print(b)
輸出:
m的id: 2092043399944
b的id: 2092043399944
[10, 20, 30]
不可變物件
傳遞引數是不可變物件,實際傳遞的是物件的引用。在”賦值操作”時,由於不可變物件無法修改,系統會新建立一個物件。
a = 100
def f2(n):
print("n的id:",id(n)) #傳遞進來的是a 物件的地址
n = n+200 #由於a 是不可變物件,因此建立新的物件n
print("n的id:",id(n)) #n 已經變成了新的物件
print(n)
f2(a)
print("a的id:",id(a))
輸出:
n的id: 1663816464
n的id: 46608592
300
a的id: 1663816464
2、淺拷貝和深拷貝
淺拷貝:不拷貝子物件的內容,只是拷貝子物件的引用。
深拷貝:會連子物件的記憶體也全部拷貝一份,對子物件的修改不會影響源物件
我們可以通過內建函式來實現:copy(淺拷貝)、deepcopy(深拷貝)。
import copy
def testCopy():
'''測試淺拷貝'''
a = [10, 20, [5, 6]]
b = copy.copy(a)
print("a", a)
print("b = copy.copy(a)")
print("b", b)
b.append(30)
b[2].append(7)
print("修改b")
print("a", a)
print("b", b)
def testDeepCopy():
'''測試深拷貝'''
a = [10, 20, [5, 6]]
b = copy.deepcopy(a)
print("a", a)
print("b = copy.deepcopy(a)")
print("b", b)
b.append(30)
b[2].append(7)
print("修改b")
print("a", a)
print("b", b)
print("-----------測試淺拷貝------------")
testCopy()
print("-----------測試深拷貝------------")
testDeepCopy()
輸出:
-----------測試淺拷貝------------
a [10, 20, [5, 6]]
b = copy.copy(a)
b [10, 20, [5, 6]]
修改b
a [10, 20, [5, 6, 7]]
b [10, 20, [5, 6, 7], 30]
-----------測試深拷貝------------
a [10, 20, [5, 6]]
b = copy.deepcopy(a)
b [10, 20, [5, 6]]
修改b
a [10, 20, [5, 6]]
b [10, 20, [5, 6, 7], 30]
3、函式的呼叫過程(棧幀)
每個函式的每次呼叫,都會在棧記憶體為這個函式建立一個棧幀(Stack Frame),用於儲存函式呼叫框架、函式引數、函式的區域性變數、函式執行完後返回到哪裡等等。
棧幀也叫過程活動記錄,是編譯器用來實現函式呼叫過程的一種資料結構。從邏輯上講,棧幀就是一個函式執行的環境。
函式呼叫完,它的棧幀即銷燬。如果函式在執行過程中又呼叫了其他函式,則在當前棧幀中再建立一個棧幀。層層巢狀。
當然,巢狀的層數是有限的。當超出範圍,就會報棧溢位異常。有時候,遞迴函式報錯就是這個原因。