Python一切皆是對象,但這和內存管理有什麼關係?

前言面試

本文的文字及圖片來源於網絡,僅供學習、交流使用,不具備任何商業用途,若有問題請及時聯繫咱們以做處理。網絡

PS:若有須要Python學習資料的小夥伴能夠點擊下方連接自行獲取數據結構

Python免費學習資料、代碼以及交流解答點擊便可加入函數


 

Python的內存管理機制

對於工程師而言,內存管理機制很是重要,是繞不過去的一環。若是你是Java工程師,面試的時候必定會問JVM。C++工程師也必定會問內存泄漏,一樣咱們想要深刻學習Python,內存管理機制也是繞不過去的一環。性能

不過好在Python的內存管理機制相對來講比較簡單,咱們也不用特別深刻其中的細節,簡單作個瞭解便可。學習

Python內存管理機制的核心就是引用計數,在Python當中一切都是對象,對象經過引用來使用。測試

 

 

咱們看到的是變量名,可是變量名指向了內存當中的一塊對象。這種關係在Python當中稱爲引用,咱們經過引用來操做對象。因此根據這點,引用計數很好理解,也就是說咱們會對每個對象進行統計全部指向它的指針的數量。若是一個對象引用計數爲0,那麼說明它沒有任何引用指向它,也就是說它已經沒有在使用了,這個時候,Python就會將這塊內存收回。指針

簡單來講引用計數原理就是這些,但咱們稍微深刻一點,來簡單看看哪些場景會引發對象引用的變化。對象

引用計數的變化顯然只有兩種,一種是增長,一種是減小,這兩種場景都只有4種狀況。咱們先來看下增長的狀況:blog

首先是初始化,最簡單的就是咱們用賦值操做給一個變量賦值。舉個例子:

 

 

這就是最簡單的初始化操做,雖然123在咱們來看是一個常數,可是在Python底層一樣被認爲是一個常數對象。n是它的一個引用。

第二種狀況是引用的傳遞,最簡單的就是咱們將一個變量的值賦值給了另一個變量。

 

 

好比咱們將n賦值給m,它的本質是咱們建立了一個新的引用,指向了一樣一塊內存。若是咱們用id操做去查看m和n的id,會發現它們的id是同樣的。也就是說它們並非存儲了兩份相同的值,而是指向了同一份值。並非有兩個叫作王小二的人,而是王小二有兩個不一樣的帳號。

第三種狀況是做爲元素被存儲進了容器當中,好比被存儲進了list當中。

 

 

雖然咱們用到了一個容器,可是容器並不會拷貝一份這些對象,仍是隻是存儲這些對象的引用。

最後一種狀況就是做爲參數傳給函數,在Python當中,全部的傳參都是引用傳遞。這也是爲何,咱們常常看到有人會這樣寫代碼的緣由:

 

 

咱們根據上面列舉的這四種引用計數增長的狀況,不難推導出引用減小的狀況, 其實基本上是對稱的操做。

和初始化對應的操做是銷燬,好比咱們建立的對象被del操做給銷燬了,那麼一樣引用計數會-1

 

 

和賦值給其餘變量名的操做相反的操做是覆蓋,好比以前咱們的n=123,也就是n這個變量指向123,如今咱們將n賦值成其餘值,那麼123這個對象的引用計數一樣會減小。

 


既然元素存儲在容器當中會帶來引用計數,那麼一樣元素從容器當中移除也會減小引用計數。這個也很好理解,最簡單的就是list調用remove方法移除一個元素:

 

最後一個對應的就是做用域,也就是當變量離開了做用域,那麼它對應的內存塊的引用計數一樣會減小。好比咱們函數調用結束,那麼做爲參數的這些變量對應的引用計數都會減1。

若是一個對象的引用計數減到0,也就是沒有引用再指向它的時候,那麼當Python進行gc的時候,這塊內存就會被釋放,也就是這個對象會被清除,騰出空間來。

注意一下,引用計數減到0與內存回收之間並非當即發生的,而是有一段間隔的。根據Python的機制,內存回收只會在特定條件下執行。在佔用內存比較小還有不少富裕的狀況下,每每是不會執行內存回收的。由於Python在執行gc(garbage collection)的時候也會stop the world,也就是暫停其餘全部的任務,因此這是影響性能的一件事情,只會在有必要的時候執行。

咱們費這麼大勁來介紹Python中的內存機制,除了向你們科普一下這一塊內容以外,更重要的一點是爲了引出咱們開發的時候常常碰見的一種狀況——循環引用。

循環引用

若是熟悉了Python的引用,來理解循環引用是很是容易的。說白了也很簡單,就是你的一個變量引用我,個人一個變量引用你。

咱們來寫一段簡單的代碼,來看看循環引用:

 

 

若是你打個斷點來看的話,會看到a和b之間的循環引用:

 


這裏是無限展開的,由於這是一個無限循環。無限循環並不會致使程序崩潰, 也不會帶來太大的問題,它的問題只有一個,就是根據前面介紹的引用計數法,a和b的引用永遠不可能爲0。

也就是說根據引用計數的原則,這兩個變量永遠不會被回收,這顯然是不合理的。雖然Python當中專門創建了機制來解決引用循環的問題,可是咱們並不知道它何時會被觸發。

這個問題在Python當中很是廣泛,尤爲在咱們實現一些數據結構的時候。舉個最簡單的例子就是樹中的節點,就是引用循環的。由於父節點會存儲全部的孩子,每每孩子節點也會存儲父節點的信息。那麼這就構成了引用循環。

 

弱引用

爲了解決這個問題,Python中提供了一個叫作弱引用的概念。弱引用本質也是一種引用,可是它不會增長對象的引用計數。也就是說它不能保證它引用的對象必定不會被銷燬,只要沒有銷燬,弱引用就能夠返回預期的結果。

弱引用不用咱們本身開發,這是Python當中集成的一個現成的模塊weakref。

這個模塊當中的方法不少,用法也不少,可是咱們基本上用不到,通常來講最經常使用的就是ref方法。經過weakref庫中的ref方法,能夠返回對象的一個弱引用。咱們仍是來看個例子:

 

 

其實仍是以前的代碼,只是作了一點簡單的改動。一個是咱們給Test加上了name這個屬性,以及str方法。另外一個是咱們把直接賦值改爲了使用weakref。

這一次咱們再打斷點進來看的話,就看不到無限循環的狀況了:

 

 

ref返回的是一個獲取引用對象的方法,而不是對象自己。因此咱們想要獲取這個對象的話,須要再把它當成函數調用一下。

固然這樣很麻煩,咱們還有更好的辦法,就是使用property註解。經過property註解,咱們能夠把weakref封裝掉,這樣在使用的時候就沒有感知了。

 

總結

引用和循環引用都是基於Python自己的機制,若是對這塊機制不瞭解,很容易採坑。由於可能會出現邏輯是對的,可是有一些意想不到的bug的狀況。這種時候,每每很難經過review代碼或者是測試發現,這也是咱們學習的瓶頸所在。很容易發現代碼已經寫得很熟練了,可是一些進階的代碼仍是看不懂或者是寫不出來,本質上就是由於缺乏了對於底層的瞭解和認知。

循環引用的問題在咱們開發代碼的時候還蠻常見的,尤爲是涉及到樹和圖的數據結構的時候。因爲循環引用的關係,頗有可能出現被刪除的樹仍然佔用着空間,內存不足的狀況發生。這個時候使用weakref就頗有必要了。

相關文章
相關標籤/搜索