1. 程式人生 > >Python——物件操作的記憶體分析

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 支援字串快取池機制,對於符合識別符號規則的字串(僅包含下劃線(_)、字母和數字)會啟用字串駐留機制駐留機制。

更多關於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),用於儲存函式呼叫框架、函式引數、函式的區域性變數、函式執行完後返回到哪裡等等。

棧幀也叫過程活動記錄,是編譯器用來實現函式呼叫過程的一種資料結構。從邏輯上講,棧幀就是一個函式執行的環境。

函式呼叫完,它的棧幀即銷燬。如果函式在執行過程中又呼叫了其他函式,則在當前棧幀中再建立一個棧幀。層層巢狀。

當然,巢狀的層數是有限的。當超出範圍,就會報棧溢位異常。有時候,遞迴函式報錯就是這個原因。

更多具體細節點這裡~