Python是一門面向對象的編程語言,Python中一切皆為對象,對每一個對象分配內存空間,Python的內存管理機制主要包括引用計數、垃圾回收和內存池機制。本文簡要介紹Python對象及內存管理機制。
參數傳遞
常見的參數傳遞有值傳遞和引用傳遞
- 值傳遞就是拷貝參數的值,然后傳遞給新變量,這樣原變量和新變量之間互相獨立,互不影響。
- 引用傳遞指把參數的引用傳給新的變量,這樣原變量和新變量指向同一塊內存地址。其中任何一個變量值改變,另外一個變量也會隨之改變。
Python 參數傳遞
Python 的參數傳遞是賦值傳遞(pass by assignment),或者叫作對象的引用傳遞(pass by object reference)。在進行參數傳遞時,新變量與原變量指向相同的對象。下面先來看一下Python中可變和不可變數據類型賦值的例子。
1. 不可變數據類型
整型(int)賦值:
a = 1 print(id(a)) b = a print(id(b)) a = a + 1 print(id(a)) c = 1 print(id(c))
執行結果:
140722100085136 140722100085136 140722100085168 140722100085136
其中id()函數用于返回對象的內存地址。
可以看到b,c都指向了相同的對象,而a = a + 1 并不是讓 a 的值增加 1,而是重新創建并指向了新的值為 2 的對象。最終結果就是a指向了2這個新的對象,b指向1,值不變。
2. 可變數據類型
以列表(list)為例:
l1 = [1, 2, 3] print(id(l1)) # l2 = l1 print(id(l2)) l1.append(4) print(id(l1)) print(l1) print(l2)
執行結果:
1933202772296 1933202772296 1933202772296 [1, 2, 3, 4] [1, 2, 3, 4]
l1 和 l2 指向相同的對象,由于列表是可變(mutable)數據類型,所以 l1.append(4)不會創建新的列表,仍然指向相同的對象。 由于l1 和 l2 指向相同的對象,所以列表變化也會導致l2的值變化。
可變對象(列表,字典,集合等)的改變,會影響所有指向該對象的變量。對于不可變對象(字符串、整型、元組等),所有指向該對象的變量的值總是一樣的,也不會改變。
Python中的'==' 和 'is'
== 和 is是Python 對象比較中常用的兩種方式,== 比較對象的值是否相等, is 比較對象的身份標識(ID)是否相等,是否是同一個對象,是否指向同一個內存地址。
a = 1 b = a print(id(a)) print(id(b)) print(a == b) print(a is b)
執行結果:
140722100085136 140722100085136 True True
a和b的值相等,并指向同一個對象。在實際應用中,通常使用== 來比較兩個變量的值是否相等。is 操作符常用來檢查一個變量是否為 None:
if a is None: print("a is None") if a is not None: print("a is not None")
Python淺拷貝和深度拷貝
前面介紹了Python的賦值(對象的引用傳遞),那么Python如何解決原始數據在函數傳遞后不受影響呢,Python提供了淺度拷貝(shallow copy)和深度拷貝(deep copy)兩種方式。
- 淺拷貝(copy):拷貝父對象,不拷貝對象內部的子對象。
- 深拷貝(deepcopy):完全拷貝了父對象及其子對象。
淺拷貝
1. 不可變數據類型
下面對不可變對象整型變量和元組進行淺拷貝:
import copy a = 1 b = copy.copy(a) print(id(a)) print(id(b)) print(a == b) print(a is b) t1 = (1, 2, 3) t2 = tuple(t1) print(id(t1)) print(id(t2)) print(t1 == t2) print(t1 is t2)
執行結果:
50622072 50622072 True True 55145384 55145384 True True
不可變對象的拷貝和對象的引用傳遞一樣,a、b指向相同的對象,修改其中一個變量的值不會影響另外的變量,會開辟新的空間。
2. 可變數據類型
對可變對象list進行淺拷貝:
import copy l1 = [1, 2, 3] l2 = list(l1) l3 = copy.copy(l1) l4 = l1[:] print(id(l1)) print(id(l2)) print(l1 == l2) print(l1 is l2) print(id(l3)) print(id(l4)) l1.append(4) print(id(l1)) print(l1 == l2) print(l1 is l2)
執行結果:
48520904 48523784 True False 48523848 48521032 48520904 False False
可以看到,對可變對象的淺拷貝會重新分配一塊內存,創建一個新的對象,里面的元素是原對象中子對象的引用。改變l1的值不會影響l2,l3,l4的值,它們指向不同的對象。
上面的例子比較簡單,下面舉一個相對復雜的數據結構:
import copy l1 = [[1, 2], (4, 5)] l2 = copy.copy(l1) print(id(l1)) print(id(l2)) print(id(l1[0])) print(id(l2[0])) l1.append(6) print(l1) print(l2) l1[0].append(3) print(l1) print(l2)
執行結果:
1918057951816 1918057949448 2680328991496 2680328991496 [[1, 2], (4, 5), 6] [[1, 2], (4, 5)] [[1, 2, 3], (4, 5), 6] [[1, 2, 3], (4, 5)]
l2 是 l1 的淺拷貝,它們指向不同的對象,因為淺拷貝里的元素是對原對象元素的引用,因此 l2 中的元素和 l1 指向同一個列表和元組對象(l1[0]和l2[0]指向的是相同的地址)。l1.append(6)不會對 l2 產生任何影響,因為 l2 和 l1 作為整體是兩個不同的對象,不共享內存地址。
l1[0].append(3)對 l1 中的第一個列表新增元素 3,因為 l2 是 l1 的淺拷貝,l2 中的第一個元素和 l1 中的第一個元素,共同指向同一個列表,因此 l2 中的第一個列表也會相對應的新增元素 3。
這里提一個小問題:如果對l1中的元組新增元素(l1[1] += (7, 8)),會影響l2嗎?
到這里我們知道使用淺拷貝可能帶來的副作用,要避免它就得使用深度拷貝。
深度拷貝
深度拷貝會完整地拷貝一個對象,會重新分配一塊內存,創建一個新的對象,并且將原對象中的元素以遞歸的方式,通過創建新的子對象拷貝到新對象中。因此,新對象和原對象沒有任何關聯,也就是完全拷貝了父對象及其子對象。
import copy l1 = [[1, 2], (4, 5)] l2 = copy.deepcopy(l1) print(id(l1)) print(id(l2)) l1.append(6) print(l1) print(l2) l1[0].append(3) print(l1) print(l2)
執行結果:
3026088342280 3026088342472 [[1, 2], (4, 5), 6] [[1, 2], (4, 5)] [[1, 2, 3], (4, 5), 6] [[1, 2], (4, 5)]
可以看到,l1 變化不影響l2 ,l1 和 l2 完全獨立,沒有任何聯系。
在進行深度拷貝時,深度拷貝 deepcopy 中會維護一個字典,記錄已經拷貝的對象與其 ID。如果字典里已經存儲了將要拷貝的對象,則會從字典直接返回。
Python垃圾回收
Python垃圾回收包括引用計數、標記清除和分代回收
引用計數
引用計數是一種垃圾收集機制,當一個python對象被引用時,引用計數加 1,當一個對象的引用為0時,該對象會被當做垃圾回收。
from sys import getrefcount l1 = [1, 2, 3] print(getrefcount(l1)) # 查看引用計數 l2 = l1 print(getrefcount(l2))
執行結果:
2 3
在使用 getrefcount()的時候,變量作為參數傳進去,會多一次引用。
del語句會刪除對象的一個引用。請看下面的例子
from sys import getrefcount class TestObjectA(): def __init__(self): print("hello!!!") def __del__(self): print("bye!!!") a = TestObjectA() b = a c = a print(getrefcount(c)) del a print(getrefcount(c)) del b print(getrefcount(c)) del c print("666")
執行結果:
hello!!! 4 3 2 bye!!! 666
方法__del__ 的作用是當對象被銷毀時調用。其中del a刪除了變量a,但是對象TestObjectA仍然存在,它還被b和c引用,所以不會被回收,引用計數為0時會被回收。上面的例子中,將a,b,c都刪除后引用的對象被回收(打印“666”之前)。
另外重新賦值也會刪除對象的一個引用。
標記清除
如果出現了循環引用,引用計數方法就無法回收,導致內存泄漏。先來看下面的例子:
class TestObjectA(dict): def __init__(self): print("A: hello!!!") def __del__(self): print("A: bye!!!") class TestObjectB(dict): def __init__(self): print("B: hello!!!") def __del__(self): print("B: bye!!!") a = TestObjectA() b = TestObjectB() a['1'] = b b['1'] = a del a del b print("666")
執行結果:
A: hello!!! B: hello!!! 666 A: bye!!! B: bye!!!
上面的代碼存在循環引用,刪除a和b之后,它們的引用計數還是1,仍然大于0,不會被回收(打印“666”之后)。
標記清除可解決循環引用問題,從根對象(寄存器和程序棧上的引用)出發,遍歷對象,將遍歷到的對象打上標記(垃圾檢測),然后在內存中清除沒有標記的對象(垃圾回收)。上面的例子中,a和b相互引用,如果與其他對象沒有引用關系就不會遍歷到它,也就不會被標記,所以會被清除。
分代回收
如果頻繁進行標記清除會影響Python性能,有很多對象,清理了很多次他依然存在,可以認為,這樣的對象不需要經常回收,也就是說,對象存在時間越長,越可能不是垃圾。
將回收對象進行分代(一共三代),每代回收的時間間隔不同,其中新創建的對象為0代,如果一個對象能在第0代的垃圾回收過程中存活下來,那么它就被放入到1代中,如果1代里的對象在第1代的垃圾回收過程中存活下來,則會進入到2代。
gc模塊
以下三種情況會啟動垃圾回收:
- 調用gc.collect():強制對所有代執行一次回收
- 當gc模塊的計數器達到閥值的時候。
- 程序退出的時候
gc 模塊函數:
- gc.enable() :啟用自動垃圾回收
- gc.disable():停用自動垃圾回收
- gc.isenabled():如果啟用了自動回收則返回 True。
- gc.collect(generation=2):不設置參數會對所有代執行一次回收
- gc.set_threshold(threshold0[, threshold1[, threshold2]]):設置垃圾回收閾值
- gc.get_count():當前回收計數
- 垃圾回收啟動的默認閾值
import gc print(gc.get_threshold())
輸出:
(700, 10, 10)
700是垃圾回收啟動的閾值,對象分配數量減去釋放數量的值大于 700 時,就會開始進行垃圾回收,每10次0代垃圾回收,會導致一次1代回收;而每10次1代的回收,才會有1次的2代回收。可以使用set_threshold()方法重新設置。
Python內存管理機制:Pymalloc
Pymalloc
Python實現了一個內存池(memory pool)機制,使用Pymalloc對小塊內存(小于等于256kb)進行申請和釋放管理。
當 Python 頻繁地創建和銷毀一些小的對象時,底層會多次重復調用 malloc 和 free 等函數進行內存分配。這不僅會引入較大的系統開銷,而且還可能產生大量的內存碎片。
內存池的概念就是預先在內存中申請一定數量的內存空間,當有有滿足條件的內存請求時,就先從內存池中分配內存給這個需求,如果預先申請的內存已經耗盡,Pymalloc allocator 會再申請新的內存(不能超過預先設置的內存池最大容量)。垃圾回收時,回收的內存歸還給內存池。這樣做最顯著的優勢就是能夠減少內存碎片,提升效率。
如果應用的內存需求大于 pymalloc 設置的閾值,那么解釋器再將這個請求交給底層的 C 函數(malloc/realloc/free等)來實現。
python內存池金字塔
- 第-1層和-2層:由操作系統操作。
- 第0層:大內存,若請求分配的內存大于256kb,使用malloc、free 等函數分配、釋放內存。
- 第1層和第2層:由python的接口函數Pymem_Malloc實現,若請求的內存在小于等于256kb時使用該層進行分配。
- 第3層(最上層):用戶對python對象的直接操作
圖片來源:https://www.c-sharpcorner.com/article/memory-management-in-python/
總結
本文主要介紹了Python的參數傳遞、淺拷貝、深拷貝,垃圾回收和內存池機制。
- Python 中參數的傳遞既不是值傳遞,也不是引用傳遞,而是賦值傳遞,或者是叫對象的引用傳遞。需要注意可變對象和不可變對象的區別。比較操作符==比較對象間的值是否相等,而`is比較對象是否指向同一個內存地址。
- 淺拷貝中的元素是對原對象中子對象的引用,如果父對象中的元素是可變的,改變它的值也會影響拷貝后的對象。深拷貝則會遞歸地拷貝原對象中的每一個子對象,是對原對象的完全拷貝。
- Python垃圾回收包括引用計數、標記清除和分代回收三種,可以使用gc模塊來進行垃圾回收的配置。為了減少內存碎片,提升效率,Python使用了Pymalloc來管理小于等于256kb的小內存。
原文地址:https://www.toutiao.com/a7067145281871266317/