Learning Python Part I 之動態類型

之前我們已經瞭解到,在Python中我們不需要提前聲明在代碼中所需要使用的變量。事實上,當我們設計程序時甚至不應該去關注特定類型,因爲Python會自動分配類型。Python是簡潔靈活的語言,而動態類型是靈活性的根本

變量、對象和引用(references)

變量創建:變量在當你在代碼中第一次賦值時被創建,當以後再次賦值時會改變變量的值
變量類型:一個變量從來不會有特定類型或相關的限制,類型的概念依存於對象,而不是變量名。變量會在需要的時候指向特定的對象。
變量的使用:當變量在表達式中出現的時候,它會立即取代它當前所指向的對象。所有變量在使用前都必須賦值,否則的話會拋出錯誤。

python的動態類型模型和傳統編程語言的聲明類型模型完全不同。如果你能區分開變量名和對象的話,這個模型很好理解。例如賦值語句a = 3,Python會通過三步完成整個過程:

  1. 創建一個對象代表 3
  2. 如果變量a不存在的話創建一個變量a
  3. 建立變量a和新的對象3的聯繫

這個結果會在Python內部以下圖的結構存在,變量名和對象儲存在內存的不同部分並且相互聯繫。變量通常只會指向一個對象而不會指向其他變量,但是大部分的對象都會指向其他對象,例如:列表對象會與它所包含的對象相聯繫。

這裏寫圖片描述

在Python中這種從變量到對象的聯繫稱作引用(references)——引用是一種聯繫或者關係,在內存中通過指針(與C語言中的指針類似)實現。整個總結起來就是:

  1. 變量是系統表中的一個條目,並帶有儲存指向對象的鏈接的空間
  2. 對象是一系列相互聯繫的有足夠空間存儲它所代表的數據的儲存
  3. 引用(references) 是變量到對象的指針

至少從概念上來說,每次你在代碼中產生一個新的值,Python就會創建一個新的對象去代表這個值。爲了最優化,Python在內部會自動的緩存和重(chong)用特定類型的不變對象,例如整數和字符串。不過從邏輯的角度,它工作起來好像每一個表達式的結果是完全不同的對象並且每個對象存儲在完全不同的內存中。技術上來說,每個對象會保留更多的空間比起它們所代表內容所需要的空間。每個對象都有兩個標準的的頭區域:一個類型標識符標註對象的類型,一個引用計數器(reference counter)決定什麼時間回收這個對象。

類型依存於對象,而不是變量

>>> a = 3    #a是整數
>>> a = 'hello'   #a是字符串
>>> a = 3.14    #a是浮點數

Python中的變量在需要的時候指向特定的對象。類型是與對象相聯繫的,而不是變量名。這也是Python的靈活性所在,如果能利用好這一點,你的代碼可以適用於幾種類型(例如函數在不改動代碼的情況下可以接受數字參數和字符串參數)

對象的垃圾回收機制(Garbage-collected)

>>> a = 3
>>> a = 'python'

在Python中,當一個變量被賦值了一個新的對象,那麼他所引用(reference)的前一個對象如果沒有被其他變量或對象引用的話就會被回收,這種自動回收無用對象所佔空間的過程被稱作垃圾回收機制(garbage collection)

>>> x = 32
>>> x = 'python'   #32被回收
>>> x = 3.145      #'python'被回收
>>> x = [1, 2, 3]  #3.145被回收
    每次x被重新賦值了一個對象,前一個對象就會被回收,所佔用的存儲空間會被清空,變成空閒空間,等待重新利用。
    在Python內部,Python通過**引用計數器**(reference counter)記錄所有當前指向該對象的引用,當計數器的值變爲零的時候,這個變量所佔用的空間就會被自動回收。

垃圾回收機制最大的好處就是你可以自由的使用對象而不用擔心垃圾
佔用空間。在程序運行的過程中Python會自動釋放棄用的空間。這幫你省掉了許多在其他語言如C、C++中的麻煩事。

循環引用cyclic reference

Python的垃圾回收機制主要是基於引用計數器。但是,Python也有一個組件通過循環引用檢測和回收對象。這個組件可以關閉,但默認是啓用的。

循環引用是一種特殊情況。因爲引用是通過指針實現的,所以一個對象有時可能會引用自身或者它所引用的其他對象存在這種情況(例如 L.append(L)),當向用戶定義的類的屬性賦值時也會出現循環引用。儘管十分罕見,但這類對象的引用計數器不會遞減到零,所以需要區別對待。但用的時可以查看手冊。

共享引用(shared references)

>>> a = 3
>>> b = a

這裏寫圖片描述
變量b指向了與變量a相同的對象,事實上在內部變量是一個指針,指向了第一個賦值語句所創建的對象的存儲區域。引用示意圖如上圖所示。

 >>> a = 3
 >>> b = a
 >>> a =  'spam'

這裏寫圖片描述
當a改變之後,b依然指向對象3,因爲數字對象是不可變對象,所以改變變量並不會改變對象,如下面這個例子,變量a會創建一個新的對象5,而不是去改變對象3.

>>> a = 3
>>> b = a
>>> a = a + 2

不像其它語言,在Python中,變量始終是指向其它對象的指針,而不是指向一塊可改變的內存區域。對於不可變對象,更新對象並不會改變原始對象,而是會指向一個新的對象。對於不可變對象則完全不同。

共享引用與可變對象

Python中對可變對象(Mutable object)——包括列表、字典和集合 的操作會改變自身而不是去產生一個新的對象。例如,對列表中的一個元素賦值會更新列表的值而不是去產生一個新的列表。在程序設計過程中應該時刻注意,可變對象的多個引用的其中一個對數據的更新會影響原始數據。否則的話你的對象可能會毫無理由的變化,例如:

>>> L1 = [1, 2, 3]
>>> L2 = L1
>>> L2[0] = 24
>>> L1
[24, 2, 3]
>>> L2
[24, 2, 3]

這種情況只會發生在可變對象身上,這也是可變對象的一個優點,但是也應該知道其中的而原理以及如何運用如果要避免這種情況,可以複製對象實現,以下是幾種方法:

>>> L1 = [1, 2, 3]
>>> L2 = L1[:]      #通過切片複製
>>> L1[0] = 24
>>> L1
[24, 2, 3]
>>> L2
[1, 2, 3]

切片操作有侷限性,它不適用與非序列的可變對象,例如字典、集合等。但也可以通過copy模塊中的方法實現:

>>> import copy
>>> X = copy.copy(Y)  #複製對象Y
>>> X = copy.deepcopy(Y)  #複製所有嵌套部分

共享引用和相等

>>> x = 42
>>> x = 'shrubbery'

因爲Python緩存和重用一些常見的整數和字符串,數字對象42不會被立即回收。相反的,它會被保存在系統表(system table)中,以便當你的代碼下次產生數字對象42時重新使用。大部分對象在不使用之後會被立即回收,對於那些例外的,他們的緩存機制可能和你的代碼沒有關係。

在Python中,有兩種等號: ==is

>>> L = [1, 2, 3]  
>>> M = L     #M和L指向一個相同的對象
>>> N = [1, 2, 3]  #N和L值相同但不是同一個對象
>>> L == M
True
>>> L is M
True
>>> L == N
True
>>> L is N
False

==檢驗兩個引用對象的值是否相同,而is檢驗的時對象的一致性,連個對象是否爲同一個對象。所以結果如上面代碼所示。然而,還有一件有趣的事情:

>>> x = 42
>>> y = 42
>>> x == y
True
>>> x is y   #證明前面兩個賦值語句產生的是同一個對象
True

因爲Python會緩存和重用一些小的整數和字符串對象。
如果往更深層的挖掘,我們查出一個對象到底有多少引用,通過sys標準庫中的getrefcount函數我們可以得到引用數,例如在 IDLE GUI中查詢整數對象1的引用量:

>>> import sys
>>> sys.getrefcount(1)
735   #這些references和你的代碼無關,是Python內部代碼的引用

弱引用(‘weak’ reference)

弱引用通過weakref標準庫實現

一個對象的弱引用並不能阻止該對象被回收,如果一個對象的最後一個引用是弱引用,那麼這個對象就會被回收,弱引用也會被刪除或者通知。這個在緩存以字典爲基本結構的大對象時會很有用,這也是引用模型的特殊情況,在需要時可以查看手冊。

總結

儘管動態類型概念看起來有點抽象,但在Python中賦值和引用(reference)隨處可見,在賦值語句、函數傳參、for循環變量、模塊引入中等等,都是基於一個賦值模型,而且是Python中僅有的一個。
在實踐上,動態類型意味着你可以寫更少的代碼。更重要的是,動態類型也是Python中多態(polymorphism)的根本。正是由於這些特性,Python纔是簡潔的高度靈活的語言!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章