動態(tài)類型
通過上一章學(xué)習(xí)的Python數(shù)字類型和操作,我們開始對Python的核心對象類型進行深入探索。我們將會在下一章繼續(xù)對象類型之旅,在繼續(xù)學(xué)習(xí)之前,掌握Python編程中最基本的概念的很重要的。動態(tài)類型以及由它提供的多態(tài)性,這些概念五一是Python語言間接性和靈活性的基礎(chǔ)。
貫穿全書,在Python中,我們并不會聲明腳本中使用的對象的確切類型。事實上,大多數(shù)程序甚至可以不在意特定的類型;相反的,它們能夠自然地適用于更廣泛的處境。由于動態(tài)語言是Python由于靈活性的根源,同時也是困擾新手的一個難題,讓我們先簡要地探索一下這個模塊。
缺少聲明語句的情況
如果你有消息編譯或靜態(tài)語言C、C++或JAVA的背景,學(xué)到這里,你也許會有些困惑,到目前為止,我們使用變量時,都沒有聲明變量的存在和類型,但變量還可以工作。例如,在交互會話模式或程序文件中,但輸入a = 3 時,Python怎么知道那代表一個整數(shù)呢?在這種情況下,Python怎么找到a是什么?
一旦你開始問這樣的問題,就已經(jīng)進入了Python動態(tài)類型模型的領(lǐng)域。在Python中,類型是在運行時自動決定的,而不是通過代碼聲明。這意味著每一必要事先聲明變量(只要記住,這個概念實質(zhì)上對變量、對象和他們之間的關(guān)系都適用,那么這個概念也就很容易了解并掌握了)。
變量、對象和引用
就像已經(jīng)使用過的很多例子一樣,當在Python中運行賦值語句a = 3時,即使沒有告訴Python將a作為一個變量來使用,或者沒有告訴它a應(yīng)該作為一個整數(shù)類型對象,一樣都能工作。在Python語言中,這些都會以一種非常自然的方式完成,就像下面這樣。
變量創(chuàng)建
一個變量,也就是變量名,就像是a,當代碼第一次給它賦值時就創(chuàng)建了它,之后的賦值將會改變已創(chuàng)建的變量名的值。從技術(shù)上來講,Python的代碼在運行之前先檢測變量名,但你可以理解為最初的賦值操作在創(chuàng)建變量。
變量類型
變量永遠不會擁有任何和它關(guān)聯(lián)的類型信息或約束。類型概念存在于對象而不是變量名中。變量永遠是通用的,它只是在一個特定的時間點,簡單的引用了一個特定對對象而已
變量使用
當變量出現(xiàn)在表達式中時,它會馬上被當前引用對象所代替,無論這個對象是什么類型。此外,所有的變量必須在使用前被明確地賦值,使用未賦值的變量會產(chǎn)生錯誤。
總而言之,變量在賦值的時候才被創(chuàng)建,它可以引用任何類型的對象,并且必須在應(yīng)用之前賦值。這意味著,不需要通過腳本聲明所需要的使用的名字,但是,必須初始化名字才能更新它們。
這種動態(tài)類型模式與傳統(tǒng)語言的類型模式相比有明顯的不同。剛?cè)腴T時,如果清除地將變量名和對象劃分開,動態(tài)類型是很容易理解的。
例如:

1.創(chuàng)建以后對象代表值3
2.創(chuàng)建一個變量a,如果它還沒有創(chuàng)建的話
3.將變量與新的對象3相連接。

在Python中變量到對象的連接稱為引用。也就是說,引用是一種關(guān)系,通過內(nèi)存中的指針的形式來實現(xiàn)。一旦引用變量,Python會自動跟蹤這個變量到對象的連接。這實際上比術(shù)語所描述的要簡單的多,以具體是術(shù)語來講
變量是一個系統(tǒng)表的人口,包含了指向?qū)ο蟮倪B接
對象是被分配到的一快內(nèi)存,有足夠的空間去表示他們所代表的值
引用是自動形成從變量到對象的指針
在程序中,每一次通過運行一個表達式生成一個新的值,Python都創(chuàng)建了一個新的對象表示這個值。從內(nèi)部來看,作為一種優(yōu)化手段,Python緩存了這一類不可變對象并對其進行復(fù)用。
例如,小的整數(shù)和字符串(每一個0都不是一塊真正的、新的內(nèi)存塊)。但是從邏輯的角度來看,這工作起來就像每一個表達式結(jié)果的值都是一個不同的對象,而每一個對象都是不同的內(nèi)存
從技術(shù)上講,對象不僅僅有足夠的空間表示他的值,還包括了更復(fù)雜的結(jié)構(gòu)。每一個對象都有兩個標準的頭部信息:類型標志符標識了這個對象的類型,引用計數(shù)器決定了何時回收這個對象。
類型屬于對象,而不是變量
當我們對一個變量進行多次賦值

a一開始是一個整數(shù),然后變成一個字符串,最后成為一個浮點數(shù)。這個例子似乎對C程序員來說可能特別奇怪,當我們說a = ‘a(chǎn)sdw'’,a的類型似乎從整數(shù)變成了字符串
事實并非如此,在Python中,情況很簡單,變量名沒有類型。
類型屬于對象,而不是變量名。我們只是把a的修改為不同對象的引用。事實上,Python的變量就是在特定的時間引用了一個特定的對象。
從另一方面來講,對象知道自己的類型,每個對象都包含了一個頭部信息,其中標記了這個對象的類型。例如,對于整數(shù)3,包含了值3以及一個標志符,告訴Python這是一個整數(shù)對象。'asdw'字符串的對象的標志符指向了一個字符串類型,因為對象記錄了它們的類型,變量就沒有必要再記錄了。
注意Python中的類型是由對象相關(guān)聯(lián)的,而不是和變量關(guān)聯(lián)。在典型的代碼中,一個給定的變量往往只會引用一種類型的對象。盡管這樣,因為這不是必須的你將會發(fā)現(xiàn)Python大媽比你通常慣用的代碼更加靈活,如果正確使用Python,代碼能夠自動以多種類型工作
對象的垃圾回收

我們將變量a賦值給了不同的類型的對象牡丹石當重新給變量a賦值時,它前一個引用值發(fā)生了什么變化?
在Python中,每一個變量名被賦予一個新的對象,如果原來的對象沒有被其他的變量名或?qū)ο笏玫脑挘敲粗暗哪莻€對象占用的空間就會被回收。
這種自動回收對象空間的技術(shù)叫做垃圾回收,也正因為這種技術(shù),Python程序員的工作得到了很大的簡化。
下面每個語句都把變量名x賦值給了不同的對象

首先注意x每次被設(shè)置為不同類型的對象,再者,盡管這并不真正的情況,效果確實x的類型每次都在改變。在Python中,類型屬于對象,而不是變量名。由于變量名只是應(yīng)用對象而已,這種代碼自然行得通
第二,注意對象的引用值在此過程中會被逐一丟棄。每一個x被賦值給一個新的對象,Python都會回收之前對象的空間。當x賦值為字符串時,對象42馬上被回收(假設(shè)沒有其他引用),對象的空間自動放入自由內(nèi)存空間池,等待后來的對象使用。
在內(nèi)部,Python是這樣實現(xiàn)這一功能的,它的每個對象中保留了一個計數(shù)器,計數(shù)器記錄當前指向該對象的引用的數(shù)目,一旦這個計數(shù)器被設(shè)置為零。這個對象的內(nèi)存空間就會自動回收。
立即回收最直接的可感受到的好處是可以在程序中任意使用對象而不需要考慮申請或釋放內(nèi)存空間。在程序運行時,Python會清理那些不再使用的空間。
關(guān)于Python垃圾回收的更多討論
實際上,Python的垃圾收集主要基于應(yīng)用計數(shù)器,正如前面介紹的。然而,它也有一部分組件可以及時的檢測并回收帶有循環(huán)引用的對象。如果你確保自己的代碼沒有產(chǎn)生循環(huán)引用,可以關(guān)閉這部分功能,但該功能默認是可用的。
循環(huán)引用是基于引用計數(shù)器的垃圾回收器需要面對的經(jīng)典問題,由于引用實現(xiàn)為指針,一個對象有可能會引用自身,或者引用另一個引用了自身的對象。如a.append(a)。對于來自用戶定義的類的對象的屬性賦值會產(chǎn)生同樣的現(xiàn)象,盡管相對及哦啊搜啊,由于這樣的對象引用計數(shù)器不會清除為0,必須特別對待他們。
共享引用

輸入這兩行命令后,生成如下所示的結(jié)果

實際的效果就是變量a和b都引用了相同的對象,也就是說,指向了相同的內(nèi)存空間
這種情況在Python中稱為共享引用,即多個變量名引用了同一個對象。注意,名字a和b此時沒有關(guān)聯(lián)。實際上,Python中不會發(fā)生兩個變量的相互關(guān)聯(lián)。

a = 'asdw'簡單的創(chuàng)建了一個新的對象,并設(shè)置a對這個新的對對象進行引用。盡管這樣,這并不會改變b的值,b仍然是原始的引用對象整數(shù)3.

a不再指向3,而是指向asdw,對于對象3有沒有影響,有,a指向3時計數(shù)器加一,現(xiàn)在a不指向3后計數(shù)器減一,當計數(shù)器為0時3被回收
Python中的變量總是一個指向?qū)ο蟮闹羔槪皇强桃飧淖兊膬?nèi)存區(qū)域的標簽,給一個變量賦一個新值,并不是替換原始的對象,而是讓這個變量去引用完全不同的一個對象。實際的效果就是對一個變量賦值,僅僅影響被賦值的那個變量
還有一些知識點需要回顧一下,數(shù)字類型是不能被更改的,你所見到的改變都不是在原位置改變的,不可變的有數(shù)字,字符串,元組。不能再原內(nèi)存地址修改。
共享引用·和在原位置修改
有一些操作對象(包括列表、字典和結(jié)婚在內(nèi)的Python可變類型)確實會在原位置改變對象。例如,在一個列表中對一個偏移進行賦值確實會改變這個列表對象,而不是生成一個新的列表對象。
或許你必須相信,這種特殊性有時候會對你的代碼造成很大的影響。對于支持這種在原位置修改的對象,共享引用時需要加倍小心,因為對一個變量名的修改會影響其他的變量,否則,你的對象坑會莫名其妙地發(fā)生改變??紤]到所有的賦值都是基于引用,這個風(fēng)險普遍存在

這里我們沒有改變a,而是改變a引用的對象的一個元素。這類修改會在位置覆蓋列表對象中的某部分值。因為這個列表都是是與其他對象共享的。那么一個像這樣的修改不僅僅會對a有影響,還會對b產(chǎn)生影響,因為a和b都引用了相同的對象。
或者我應(yīng)該換一個講法,拋去書本,這里介紹一個函數(shù)id。它可以返回響應(yīng)的內(nèi)存地址

這里第一個a的內(nèi)存地址和第二個a的內(nèi)存地址不一樣,因為a指向的東西不一樣,然后是將b賦值為a,所以一樣的內(nèi)存地址都是指向列表,然后我們創(chuàng)建了c,其內(nèi)容表面上和a相同,但是內(nèi)存地址不同,這表明這里c創(chuàng)建了新的對象。然后我們查看了列表中所有元素的內(nèi)存地址,發(fā)現(xiàn)和列表的內(nèi)存地址不同,這表明列表和列表中元素存儲在不同的內(nèi)存地址。
對于列表,字典,它們的外殼和內(nèi)容不是同一個內(nèi)存地址。可以看一下我以前寫的列表和列表中的元素?鏈接??,F(xiàn)在看的確那時候?qū)W識淺薄,看一樂就好了。
共享引用和相等
對于之前提到的垃圾回收機制,對于特定的類型而言可能更加概念化而不是完全遵照條規(guī)。

因為Python緩存并復(fù)用了小的整數(shù)和小的字符串,就像前文提到的那樣,這里的42也許不會被回收,可能被保存在一個系統(tǒng)表中,等待下一次你的代碼生成另一個42除法利用。盡管如此,大多數(shù)種類的對象都會在不再次引用時馬上回收,對于那些不會被回收的對象,緩存機制并不影響你所編寫的代碼
例如,基于Python的引用模型,在python中有兩種不同的方法去檢查是否相等,

這里的第一種技術(shù)“==”運算符,測試兩個被引用對象是否有相同的值。這種方法在python中用作相等的檢查,第二種方法is運算符,是在檢查對象的同一性,如果一個變量名精確的指向同一個對象,他會返回True。這是一種更嚴格形式的相等測試,它在大多數(shù)程序中很少出現(xiàn)。
== 就可以翻譯成等于,而is應(yīng)該翻譯成就是。打個比方,我兜里的100塊錢等于你兜里的100塊錢,但是不能說我兜里的100就是你兜里的100。對于錢來說它的編號不同,對于Python來說這兩個整數(shù)100的內(nèi)存地址不同。

x is y為真是因為對于對象42被Python緩存并重復(fù)利用了,而對于a is b錯誤是因為對于數(shù)值相同的列表它們的內(nèi)存地址不同,沒有被Python緩存復(fù)用。

如果你真的想刨根問底,查詢一個對象引用的次數(shù),可以使用sys模塊的getrefcount函數(shù)。

例如這里查詢1的引用次數(shù),返回的結(jié)果是868,部分IDLE系統(tǒng)代碼所使用的,而非你自己的代碼,這里返回868,說明Python自己也私藏了很多。
這種對象緩存和復(fù)用的機制和代碼是無關(guān)的。因為不能再原位置改變的有數(shù)字和字符串,所以無論對同一個對象有多少個引用都沒有關(guān)系,所有的引用都會看到同樣的不可改變的值。
然而,這種現(xiàn)象也反映了Python為了執(zhí)行速度而采用的優(yōu)化模式的眾多方法的一種。
動態(tài)類型隨處可見
在使用Python的過程中,你沒有必要用圓圈和箭頭探查變量名和對象的關(guān)系。盡管目前來說動態(tài)類型看起來有些抽象,你最終還是要關(guān)注它的,因為在Python中,任何東西看起來都是通過賦值和引用工作的,對這個模型有基本了解在不同場合下還是很有幫助的。就像你看到的那樣,它也會在賦值語句,變量參數(shù),for循環(huán)變量,模塊導(dǎo)入,類屬性等很多場合發(fā)揮作用,值得高興的是,這是Python中唯一的賦值模型,一旦你對動態(tài)類型上手了,將會發(fā)現(xiàn)它在這門語言中任何地方都有效。
從最實際的角度來說,動態(tài)類型類型意味著你可以寫更少的代碼。動態(tài)類型也是Python中多態(tài)的根本。Python代碼中沒有對類型的約束,它具備了精確性和高度的靈活性。
弱引用
你可能偶爾會在Python世界中看到弱引用,這是一個相對高級的工具,但也與我們在這里討論的模型有關(guān)系,就像is運算符,離開了它就沒有辦法理解。
簡單來說,弱引用就是通過weakref標準庫來實現(xiàn)的一種用于防止對象被垃圾回收的引用(這一引用并非來源于自身)。如果對象的最后一次引用是弱引用,那么這個對象將被重新聲明,而相應(yīng)的弱引用會被自動刪除(或者告知)。
例如,這對于字典的大對象緩存來說十分有用。否則,單靠緩存的引用并不能確保對象存在于內(nèi)存中。這種情況仍然可以視作引用模型的一種特例。
本章小結(jié)
本章對Python的動態(tài)類型模型進行了深入的學(xué)習(xí)。在這個過程中,我們學(xué)會了Python中變量和對象是如何通過引用關(guān)聯(lián)在一起的,還探索了垃圾收集的概念,學(xué)到了對象共享引用是如何影響多個變量的,并看到了Python中引用是如何影響相等概念的,
總結(jié)一下
首先聲明一下變量對象和引用的關(guān)系,了解一下賦值,類型屬于對象,每個對象都包含兩個標準的頭部信息,一個是類型標志符,確定類型,不用自己聲明,還有一個計數(shù)器,用于垃圾收集,了解一下垃圾回收機制。關(guān)于引用和原位置修改,為了更好的理解我介紹了一個id函數(shù),關(guān)于原位置修改列表和列表中內(nèi)容不是同一個對象。==運算符和is函數(shù)判斷等于,就是。聽說一個弱引用。
背誦并默寫本章習(xí)題
本章習(xí)題
1.思考下面三條語句,它們會改變A打印出的值嗎?
A = 'apam'
B = A
B = 'shurubbery'
不會:A仍然會作為'spam'進行打印,當B賦值為字符串'shrubbery'時,所發(fā)生的將是變量B被重新設(shè)置為指向性的字符串對象。A和B最初共享(即引用或指向)了同一個字符串對象'spam',但是在Python中這兩個變量名從未連接在一起。因此,設(shè)置B為了一個不同的對象對A沒有影響。如果這里最后的語句變?yōu)锽 = B + 'shrubbery',也會發(fā)生同樣的事情。順便提一句,合并操作創(chuàng)建了一個新的對象作為其結(jié)果,并將這個值賦給了B。我們永遠都不會在原位置上覆蓋一個字符串(數(shù)字或元組),因為字符串是不可變的。
2.思考下面三條語句,它們會改變A的值嗎?
A = ['spam']
B = A
B[0] = 'shrubbery'
會:A現(xiàn)在打印為['shrubbery']。從技術(shù)上講,我們既沒有改變A也沒有改變B,我們改變的是這兩個變量的共同引用(指向)的對象的一部分,通過變量B在原位置覆蓋了這個對象的一部分內(nèi)容。因為A像B一樣引用了同一個對象,這個改變也會對A產(chǎn)生影響。
3.這樣如何,A會改變嗎?
A = ['spam']
B = A[:]
B[0] = 'shrubbery'
不會:A仍然會打印為['spam']。由于分片表達式語句會在A被賦值給B前創(chuàng)建一個副本,這樣對B在原位置賦值就不會有影響了。在第二個賦值語句后,就有了兩個擁有相同值的不同列表對象(在Python中,我們說它們是==,卻不是is的)。第三條賦值語句會改變指向B的列表對象,而不會改變A的列表對象。