1. 程式人生 > 其它 >Python變數及垃圾回收機制

Python變數及垃圾回收機制

Python的註釋語法

註釋是程式碼之母,學習任何一門程式語言之前都應該先學註釋。

1.什麼是註釋

註釋就是對程式碼的解釋說明,註釋的內容不會被當做程式碼執行,只起到提示作用

2.為什麼要註釋

增強程式碼的可讀性

3.怎麼用註釋

程式碼註釋分為單行和多行註釋

  • 方式1:單行註釋使用"#"號,可以跟在程式碼的正上方或者正後方
  • 方式2:多行註釋可以用三對引號(單雙引號都可以)
  • 方式3:PyCharm註釋快捷鍵—— Ctrl + Alt + L

4.程式碼註釋原則

  • 不用全部加註釋,只需要為自己覺得重要或不好理解的部分加註釋即可
  • 註釋可以用中文或英文,但不要用拼音
  • “#”號與註釋文字之間一定要有一個空格
  • 如果單行註釋跟在了一行程式碼的後面,需要先空兩個空格,再寫註釋
# python程式碼編寫規範  >>>: PEP8規範
# 如何快速掌握:藉助於pycharm的自動化提示 前後對比 每天記憶即可

變數

1.什麼是變數

變數就是可以變化的量,量指的是事物的狀態,比如人的年齡、性別,遊戲角色的等級、金錢等等

2.為什麼要有變數

  • 為了讓計算機能夠像人一樣去記憶事物的某種狀態,並且狀態是可以發生變化的
  • 程式執行的本質就是一系列狀態的變化,變是程式執行的直接體現,所有我們需要有一種機制能夠反映或者說是儲存下來程式執行時的狀態,以及狀態的變化。

3.怎麼使用變數(先定義、後使用)

  • 變數的定義與使用

    • 定義變數示範如下

    • 直譯器執行到變數定義的程式碼時會申請記憶體空間存放變數值,然後將變數值的記憶體地址繫結給變數名,以變數的定義age=18為例,如下圖

      """
      語法格式
      	username = 'Jason'
      	變數名 賦值符號 變數值
      
      底層原理
      	遇到賦值符號先看符號右邊的再看左邊的
      		age = 18
      			1.在記憶體空間中申請一塊記憶體空間儲存18
      			2.將18所在的記憶體空間地址繫結給變數名age
      			3.之後如果要訪問18就通過變數名age訪問即可
      """
      

      通過變數名即可引用到對應的值

  • 變數名的命名規範

    • 變數名只能由數字、字母、下劃線任意組合
    • 變數名不能以數字開頭、建議不要以下劃線開頭(有特殊含義)
    • 變數名不能與關鍵字衝突,常見關鍵字如下
    ['and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from','global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield']
    
    • 變數名的命名一定要做到見名知意(見名知意是核心,無論變數多長)
    # 如果我們要儲存的資料18代表的是一個人的年齡,那麼變數名推薦命名為age
    age = 18 
    # 如果我們要儲存的資料18代表的是一個人的等級,那麼變數名推薦命名為level
    level = 18
    
  • 變數名的命名風格

    • 駝峰體

      大駝峰:所有單詞首字母大寫

      小駝峰:第一個字母小寫其餘首字母大寫

    • 下劃線:單詞與單詞之間下劃線隔開,Python推薦使用該風格

  • 變數值的三大特性

    • 變數的值:value
    • 變數的記憶體地址:id
    • 變數的資料型別:type

    檢視變數值三大特性的方式如下,我們將會在運算中用到變數值的三大特性:

常量

1.什麼是常量?

常量指在程式執行過程中不會改變的量。

2.為什麼要有常量?

在程式執行過程中,有些值是固定的、不應該被改變,比如圓周率 3.1415926…

3.怎麼使用常量

在Python中沒有真正意義上的常量,約定俗成是用全部大寫的變數名錶示常量。如PI = 3.1415926。所以單從語法層面去講,常量的使用與變數完全一致。

PS:在其他程式語言中是存在真正意義上的常量,定義了就無法修改。

Python底層優化機制

# 當值資料量很小的時候 如果有多個變數名需要使用 那麼會指向同一塊地址
"""
一個變數名只能指向一個記憶體地址
一個記憶體地址可以有多個變數名指向
"""

Python垃圾回收機制

直譯器在執行到定義變數的語法時,會申請記憶體空間來存放變數的值,而記憶體的容量是有限的,這就涉及到變數值所佔用記憶體空間的回收問題,當一個變數值沒有用了(簡稱垃圾)就應該將其佔用的記憶體給回收掉,那什麼樣的變數值是沒有用的呢?

由於變數名是訪問到變數值的唯一方式,所以當一個變數值不再關聯任何變數名時,我們就無法再訪問到該變數值了,該變數值就是沒有用的,就應該被當成一個垃圾回收。毫無疑問,記憶體空間的申請與回收是非常耗費精力的事情,而且存在很大的危險性,稍有不慎就有可能引發記憶體溢位問題,好在Python直譯器提供了自動的垃圾回收機制來幫我們解決了這件事。

1.什麼是垃圾資料,什麼是垃圾回收機制?

  • 垃圾資料是在記憶體中沒有任何變數名指向的資料;
  • 垃圾回收機制(簡稱GC)是Python直譯器自帶的一種機制,專門用來回收不可用的變數值所佔用的記憶體空間。

2.為什麼要用垃圾回收機制?

程式執行過程中會申請大量的記憶體空間,而對於一些無用的記憶體空間如果不及時清理的話會導致記憶體使用殆盡(記憶體溢位),導致程式崩潰,因此管理記憶體是一件重要且繁雜的事情,而Python直譯器自帶的垃圾回收機制把程式設計師從繁雜的記憶體管理中解放出來。

3.理解GC原理需要儲備的知識

3.1堆區與棧區

在定義變數時,變數名與變數值都是需要儲存的,分別對應記憶體中的兩塊區域:棧區與堆區

  • 變數名與值記憶體地址的關聯關係存放於棧區
  • 變數值存放於堆區,記憶體管理回收的則是堆區的內容

定義了兩個變數x = 10、y = 20,詳解如下圖

當我們執行x = y時,記憶體中的棧區與堆區變化如下

3.2直接引用與間接引用

直接引用指的是從棧區出發直接引用到的記憶體地址

間接引用指的是從棧區出發引用到堆區後,再通過進一步引用才能到達的記憶體地址

示例如下:

L2 = [20, 30]  # 列表本身被變數名L2直接引用,包含的元素被列表間接引用
x = 10  # 值10被變數名x直接引用
L1 = [x, L2]  # 列表本身被變數名L1直接引用,包含的元素被列表間接引用

圖解如下:

4.垃圾回收機制原理分析

Python的GC模組主要運用了“引用計數”(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,還可以通過“標記-清除”(mark and sweep)解決容器物件可能產生的迴圈引用的問題,並且通過“分代回收”(generation collection)以空間換取時間的方式來進一步提高垃圾回收的效率。

4.1引用計數

引用計數就是:變數值被變數名關聯的次數

如:age = 18

變數值18被關聯了一個變數名age,稱之為引用計數為1

引用計數增加:

age=18 (此時,變數值18的引用計數為1)

m=age (把age的記憶體地址給了m,此時,m,age都關聯了18,所以變數值18的引用計數為2)

引用計數減少:

age=10(名字age先與值18解除關聯,再與10建立了關聯,變數值18的引用計數為1)

del m(del的意思是解除變數名x與變數值18的關聯關係,此時,變數18的引用計數為0)

值18的引用計數一旦變為0,其佔用的記憶體地址就應該被直譯器的垃圾機制回收。

4.2引用計數的問題與解決方案

變數值被關聯次數的增加或減少,都會引發引用計數機制的執行(增加或減少值的引用計數),這存在明顯的效率問題。

  • 問題一:迴圈引用

    引用計數機制存在著一個致命的弱點,即迴圈引用(也稱交叉引用)

    # 如下我們定義了兩個列表,簡稱列表1與列表2,變數名L1指向列表1,變數名L2指向列表2
    >>> L1=['xxx']  # 列表1被引用一次,列表1的引用計數變為1   
    >>> L2=['yyy']  # 列表2被引用一次,列表2的引用計數變為1   
    >>> L1.append(L2)             # 把列表2追加到L1中作為第二個元素,列表2的引用計數變為2
    >>> L2.append(L1)             # 把列表1追加到L2中作為第二個元素,列表1的引用計數變為2
     
    # L1與L2之間有相互引用
    # L1 = ['xxx'的記憶體地址,列表2的記憶體地址]
    # L2 = ['yyy'的記憶體地址,列表1的記憶體地址]
    >>> L1
    ['xxx', ['yyy', [...]]]
    >>> L2
    ['yyy', ['xxx', [...]]]
    >>> L1[1][1][0]
    'xxx'
    

    迴圈引用會導致:值不再被任何名字關聯,但是值的引用計數並不會為0,應該被回收但不能被回收,什麼意思呢?試想一下,請看如下操作

    >>> del l1 # 列表1的引用計數減1,列表1的引用計數變為1
    >>> del l2 # 列表2的引用計數減1,列表2的引用計數變為1
    

    此時,只剩下列表1與列表2之間的相互引用,

    但此時兩個列表的引用計數均不為0,但兩個列表不再被任何其他物件關聯,沒有任何人可以再引用到它們,所以它倆佔用記憶體空間應該被回收,但由於相互引用的存在,每一個物件的引用計數都不為0,因此這些物件所佔用的記憶體永遠不會被釋放,所以迴圈引用是致命的,這與手動進行記憶體管理所產生的記憶體洩露毫無區別。
    所以Python引入了“標記-清除” 與“分代回收”來分別解決引用計數的迴圈引用與效率低的問題

  • 解決方案:標記-清除

    容器物件(比如:list,set,dict,class,instance)都可以包含對其他物件的引用,所以都可能產生迴圈引用。而“標記-清除”計數就是為了解決迴圈引用的問題。

    標記/清除演算法的做法是當應用程式可用的記憶體空間被耗盡的時,就會停止整個程式,然後進行兩項工作,第一項則是標記,第二項則是清除

    #1、標記
    通俗地講就是:
    棧區相當於“根”,凡是從根出發可以訪達(直接或間接引用)的,都稱之為“有根之人”,有根之人當活,無根之人當死。
     
    具體地:標記的過程其實就是,遍歷所有的GC Roots物件(棧區中的所有內容或者執行緒都可以作為GC Roots物件),然後將所有GC Roots的物件可以直接或間接訪問到的物件標記為存活的物件,其餘的均為非存活物件,應該被清除。
     
    #2、清除
    清除的過程將遍歷堆中所有的物件,將沒有標記的物件全部清除掉。
    

    基於上例的迴圈引用,當我們同時刪除L1與L2時,會清理掉棧區中L1與L2的內容以及直接引用關係

    這樣在啟用標記清除演算法時,發現棧區內不再有L1與L2(只剩下堆區內二者的相互引用),於是列表1與列表2都沒有被標記為存活,二者會被清理掉,這樣就解決了迴圈引用帶來的記憶體洩漏問題。

  • 問題二:效率問題

    基於引用計數的回收機制,每次回收記憶體,都需要把所有物件的引用計數都遍歷一遍,這是非常耗時間的,於是引入了分代回收來提高回收效率,分代回收採用的是用“空間換時間”的策略。

  • 解決方案:分代回收

    分代:

    分代回收的核心思想是:在歷經多次掃描的情況下,都沒有被回收的變數,Gc機制就會認為,該變數是常用變數,Gc對其掃描的頻率會降低,具體實現原理如下:

    • 分代指的是根據存活時間來為變數劃分不同等級(也就是不同的代)。
    • 新定義的變數,放到新生代這個等級中,假設每隔1分鐘掃描新生代一次,如果發現變數依然被引用,那麼該物件的權重(權重本質就是個整數)加一,當變數的權重大於某個設定得值(假設為3),會將它移動到更高一級的青春代,青春代的Gc掃描的頻率低於新生代(掃描時間間隔更長),假設5分鐘掃描青春代一次,這樣每次Gc需要掃描的變數的總個數就變少了,節省了掃描的總時間,接下來,青春代中的物件,也會以同樣的方式被移動到老年代中。也就是等級(代)越高,被垃圾回收機制掃描的頻率越低。

    回收:

    回收依然是使用引用計數作為回收的依據

    雖然分代回收可以起到提升效率的效果,但也存在一定的缺點:

    • 例如一個變數剛剛從新生代移入青春代,該變數的繫結關係就解除了,該變數應該被回收,但青春代的掃描頻率低於新生代,這就到導致了應該被回收的垃圾沒有得到及時的清理。

    • 沒有十全十美的方案:
      毫無疑問,如果沒有分代回收,即引用計數機制一直不停地對所有變數進行全體掃描,可以更及時地清理掉垃圾佔用的記憶體,但這種一直不停地對所有變數進行全體掃描的方式效率極低,所以我們只能將二者中和。

  • 綜上
    垃圾回收機制是在清理垃圾&釋放記憶體的大背景下,允許分代回收以極小部分垃圾不會被及時釋放為代價,以此換取引用計數整體掃描頻率的降低,從而提升其效能,這是一種以空間換時間的解決方案。