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