Python可以視作同時支持像C++一樣的RAII特性,也具有垃圾回收GC的編程語言嗎?

RAII ( Resource Acquisition Is Initialization ) 在 Python 中自動使用了引用計數來達到 RAII的效果,但是主流的Python代碼寫法似乎並不鼓勵使用 __del__ 方法?


垃圾回收只是內存管理,而 RAII 是一個資源管理工具,約束在代碼執行走出特定作用域之後,不管是正常流程,還是異常流程,都不會漏掉資源的釋放。多數情況下,都應該儘早釋放資源,而不應該依賴垃圾收集不可控的生命周期,比如文件描述符、資料庫連接。

python 在 RAII 對應的工具是 with 語句,和 try.. catch 語句中的 finally 子句。go 的 defer 讓很多人覺得很新鮮,不是么。


C++ RAII 的強大之處在於資源的生命期可以大於一個函數,目前其他語言的 finally/with/using/defer 語句都最多只能在函數 scope 內實現 RAII 的效果,跟 C++ 比差遠了。


CPython implementation detail: CPython currently uses a reference-counting scheme with (optional) delayed detection of cyclically linked garbage, which collects most objects as soon as they become unreachable, but is not guaranteed to collect garbage containing circular references. See the documentation of the gc module for information on controlling the collection of cyclic garbage. Other implementations act differently and CPython may change. Do not depend on immediate finalization of objects when they become unreachable (so you should always close files explicitly).

以上引用自 3. Data model

RAII所必需的析構操作不能保證,還有下面object.__del__里的灰框紅框,字裡行間都是個「坑」字。

with語句的__enter__和__exit__才是保證被調用的,請用這個。


RAII的要求是嚴格綁定資源的有效期與變數的生命期,這就要求語言能自動調用析構函數,也就是通常所說的確定性釋放。

__del__只能算是對象的析構函數,而python本身並未提供自動調用__del__的功能,所以,寫了__del__不代表它就是RAII。Python里真正具有RAII功能的是with語句,它能做到嚴格綁定兩者的生命期。

但正如 @陳碩 提到的那樣,with中的變數只能存在於本身的scope範圍內,這相對C++來說就非常局限了。而C++里大量使用值語義,從而可以非常自由地控制對象生命期。

GC語言里用RAII通常都有這個限制,對這個限制來說,D語言算是比較好一點的,scope(failure)可以間接繞開該限制。


我用專欄里的一篇文章來答題。

專欄鏈接:給妹子講python,歡迎大家關注,提意見!

這裡我聊聊python中的垃圾收集機制。想系統的了解這個問題我們需要涉及引用機制、動態類型和共享引用這些基本概念。

先談談python中的對象引用機制和動態類型。的確,python使用變數的時候都沒有聲明變數的類型,這一點和C語言不同。但是,變數還可以工作,因為在python中類型是在運行的過程中自動決定的,而不是通過代碼聲明的,這意味著沒有必要事先聲明變數。

在python中,我們要明確一個概念:變數名和對象是劃分開的,變數名永遠沒有任何關聯的類型信息,類型是和對象關聯的,而不存在於變數名中。一個變數名當第一次被賦值的時候被創建,而當新的賦值表達式出現時,他會馬上被當前新引用的對象所代替。這就是python所謂的動態類型機制。具體看一個例子:

a = "abcde"
print(a)
a = [1,2,3,4,5]
print(a)

abcde
[1, 2, 3, 4, 5]

結合上面這個例子,我們再來從頭仔細理一理:

1、創建了一個字元串對象』abcde』,然後創建了一個變數a,將變數a和字元串對象』abcde』相連接,

2、之後又創建了一個列表對象[1,2,3,4,5],然後又將他和a相連接。

這種從變數到對象的連接,我們稱之為引用,以內存中的指針形式實現。因此直白的說,在內部,變數事實上是到對象內存空間的一個指針,而且指向的對象可以隨著程序賦值語句而不斷變化。

總結一下:變數名沒有類型,只有對象才有類型,變數只是引用了不同類型的對象而已。每一個對象都包含了兩個頭部信息,一個是類型標誌符,標識這個對象的類型,以及一個引用的計數器,用來表示這個對象被多少個變數名所引用,如果此時沒有變數引用他,那麼就可以回收這個對象。

基於上面談到的引用機制,我們再說說Python的垃圾收集機制

還是上面那個例子,每當一個變數名被賦予了一個新的對象,那麼之前的那個對象佔用的空間就會被回收,前提是如果他沒有被其他變數名或者對象引用。這種自動回收對象空間的機制叫做垃圾收集機制。

即當a被賦值給列表對象[1,2,3,4,5]時,字元串對象的內存空間就被自動回收(如果他沒有被別的變數引用)

具體的內部機制是這樣的:python在每個對象中保存了一個計數器,計數器記錄了當前指向該對象的引用的數目。一旦這個計數器被設置為0,這個對象的內存空間就會自動回收。當a被賦值給列表對象後,原來的字元串對象『abcde』的引用計數器就會變為0,導致他的空間被回收。這就使得我們不必像C++那樣需要專門編寫釋放內存空間的代碼了

最後再說說共享引用的內容吧:

如下所示,多個變數名引用了同一個對象,稱為共享引用

a = "abcde"
b = a
print(a)
print(b)

abcde
abcde

此時字元串對象』abcde』的引用計數是2,我們進一步往下看如果我們此時對變數a重新賦值呢?

a = "abcde"
b = a
a = [1,2,3,4]
print(a)
print(b)

[1, 2, 3, 4]
Abcde

結果是顯而易見的,變數a變成了列表對象的引用,而變數b依然是字元串對象』abcde』的引用,並且字元串對象的引用計數為由2變為1.

如果此時再對b進行重新賦值,字元串對象』abcde』的引用計數就會變為0,然後這個對象就被垃圾回收了。

總結一下,給一個變數賦一個新的值,並不是替換原始的對象,而是讓這個變數去引用完全不同的另一個對象,而原來的對象的引用計數會隨之減1。


Python的with只是有限制的RAII,作用範圍是scope。C++的RAII是可以跨越函數,通過智能指針(shared_ptr之類)/Handle做到資源跨函數並在最後一個引用資源的對象析構後保證資源被立即釋放。


我感覺不能,因為這些帶gc的語言你並不能用隱式的方法控制它什麼時候釋放。沒法嚴格控制釋放時機的RAII並沒什麼意義


推薦閱讀:

非同步操作時的內存管理?
是否有可能發現一段內存被野指針修改了?
如何設計內存池?
為什麼調用 std::map::clear() 後內存佔用率沒有降低?
最近電腦每次開機內存佔用與上次關機時差不多,只有重啟後內存才會降到20左右,這是怎麼回事啊?

TAG:Python | 內存管理 | GC垃圾回收計算機科學 |