在程序開發(fā)中,我們每時(shí)每刻都在創(chuàng)建對象,那到底什么是對象呢?
其實(shí)一個(gè)對象就是一片被分配的內(nèi)存空間,空間可以是連續(xù)的,也可以是不連續(xù)的。然后空間里面存儲了指定的數(shù)據(jù),并提供了操作數(shù)據(jù)的一些功能方法。而按照是否可變和內(nèi)存大小是否固定,我們可以將對象進(jìn)行如下分類。
下面來詳細(xì)解釋一下。
不可變對象一旦創(chuàng)建,其內(nèi)存中存儲的值就不可以再修改了。如果想修改,只能創(chuàng)建一個(gè)新的對象,然后讓變量指向新的對象,所以前后的地址會發(fā)生改變。而可變對象在創(chuàng)建之后,其存儲的值可以動態(tài)修改。
像整數(shù)就是一個(gè)不可變對象。
>>> a = 666>>> id(a)2230564873872>>> a += 1>>> id(a)2230564873808
我們看到執(zhí)行 a += 1 操作之后,前后地址發(fā)生了變化,所以整數(shù)不支持本地修改,因此是一個(gè)不可變對象;
圖片
原來 a = 666,而我們說操作一個(gè)變量等于操作這個(gè)變量指向的內(nèi)存,所以 a+=1 會將 a 指向的整數(shù)對象 666 和 1 進(jìn)行加法運(yùn)算,得到 667。因此會開辟新的空間來存儲 667,然后讓 a 指向這片新的空間。至于原來的 666 所占的空間怎么辦,解釋器會看它的引用計(jì)數(shù),如果不為 0 代表還有變量引用(指向)它,如果為 0 證明沒有變量引用了,所以會被回收。
關(guān)于引用計(jì)數(shù),我們后面會詳細(xì)說,目前只需要知道當(dāng)一個(gè)對象被一個(gè)變量引用的時(shí)候,那么該對象的引用計(jì)數(shù)就會加 1。有幾個(gè)變量引用,那么它的引用計(jì)數(shù)就是幾。
除了整數(shù)之外,浮點(diǎn)數(shù)、字符串、布爾值等等,都是不可變對象,它們的值不能本地修改。
然后是可變對象,像列表、字典、集合等都是可變對象,它們支持動態(tài)修改。
這里先多提一句,Python 的對象本質(zhì)上就是 C 中 malloc 函數(shù)為結(jié)構(gòu)體實(shí)例在堆區(qū)申請的一塊內(nèi)存。Python 的任何對象在 C 中都會對應(yīng)一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體除了存放具體的值之外,還存放了一些額外的信息,這個(gè)我們在后續(xù)剖析內(nèi)置對象的時(shí)候會細(xì)說。
在上一篇文章中我們說到,列表、元組、集合這些容器的內(nèi)部存儲的不是具體的對象,而是對象的指針。比如:lst = [1, 2, 3],你以為列表存儲的是三個(gè)整數(shù)對象嗎?其實(shí)不是的,它存儲的是三個(gè)整數(shù)對象的指針,當(dāng)我們使用 lst[0] 的時(shí)候,拿到的是一個(gè)指針,但是操作(比如 print)的時(shí)候會自動操作指針指向的內(nèi)存。
因?yàn)?Python 底層是 C 來實(shí)現(xiàn)的,所以列表的實(shí)現(xiàn)必然要借助 C 的數(shù)組。可 C 數(shù)組里面的元素的類型必須一致,但列表卻可以存放任意的元素,因此從這個(gè)角度上講,列表里面的元素就不可能是對象,因?yàn)椴煌膶ο笤诘讓訉?yīng)的結(jié)構(gòu)體是不同的,所以元素只能是指針。
可能有人又好奇了,不同對象的指針也是不同的啊,是的,但 C 指針是可以轉(zhuǎn)化的。Python 底層將所有對象的指針,都轉(zhuǎn)成了 PyObject 類型的指針,這樣不就是同一種類型的指針了嗎?關(guān)于這個(gè) PyObject,它是我們后面要剖析的重中之重,貫穿了整個(gè)系列。不過目前只需要知道列表(還有其它容器)存儲的元素、以及 Python 的變量,它們都是一個(gè)泛型指針 PyObject *。
>>> lst = [1, 2, 3]>>> id(lst)2287192570048>>> lst.append(4)>>> lst[1, 2, 3, 4]>>> id(lst)2287192570048
我們看到列表在添加元素的時(shí)候,前后地址并沒有改變。列表在 C 中是通過 PyListObject 結(jié)構(gòu)體實(shí)現(xiàn)的,我們在介紹列表的時(shí)候會細(xì)說。這個(gè) PyListObject 內(nèi)部除了一些基本信息之外,還維護(hù)了一個(gè) PyObject 的二級指針,指向了 PyObject * 類型的數(shù)組的首元素。
圖片
顯然圖中的指針數(shù)組用來存儲具體的對象的指針,每一個(gè)指針都指向了相應(yīng)的對象(這里是整數(shù)對象)。
然后我們還可以看到一個(gè)現(xiàn)象,那就是列表在底層是分開存儲的,因?yàn)?PyListObject 結(jié)構(gòu)體實(shí)例并沒有存儲相應(yīng)的指針數(shù)組,而是存儲了一個(gè)二級指針。顯然添加、刪除、修改元素等操作,都是通過這個(gè)二級指針來間接操作指針數(shù)組。
因?yàn)橐粋€(gè)對象一旦被創(chuàng)建(任何語言都是如此),那么它在內(nèi)存中的大小就不可以變了。所以這就意味著那些可以容納可變長度數(shù)據(jù)的可變對象,要在內(nèi)部維護(hù)一個(gè)指針,指針指向一片內(nèi)存區(qū)域,該區(qū)域存放具體的數(shù)據(jù)。如果空間不夠了,那就申請一片更大的內(nèi)存區(qū)域,然后將元素依次拷貝過去,再讓指針指向新的內(nèi)存區(qū)域。而列表的底層也是這么做的,其內(nèi)部并沒有直接存儲具體的指針數(shù)組,而是存儲了指向指針數(shù)組首元素的二級指針。
那么問題來了,為什么要這么做?
其實(shí)很好理解,遵循這樣的規(guī)則可以使通過指針維護(hù)對象的工作變得非常簡單。一旦允許對象的大小可在運(yùn)行期改變,那么我們就要考慮如下場景。
在內(nèi)存中有對象 A,并且其后面緊跟著對象 B。如果在運(yùn)行的某個(gè)時(shí)候,A 的大小增大了,這就意味著必須將 A 整個(gè)移動到內(nèi)存中的其他位置,否則 A 增大的部分會覆蓋掉原本屬于 B 的數(shù)據(jù)。但要將 A 移動到內(nèi)存的其他位置,那么所有指向 A 的指針就必須立即得到更新。可想而知這樣的工作是多么的繁瑣,因此通過在可變對象的內(nèi)部維護(hù)一個(gè)指針就變得簡單多了。
所謂定長和變長,取決于對象所占的內(nèi)存大小是否固定,舉個(gè)例子。
>>> import sys>>> sys.getsizeof("")41>>> sys.getsizeof("hello")46>>> sys.getsizeof("hello world")52>>> sys.getsizeof(1.0)24>>> sys.getsizeof(3.14)24>>> sys.getsizeof((2 << 30) + 3.14)24
我們看到字符串的長度不同,所占的內(nèi)存也不同,像這種內(nèi)存大小不固定的對象,我們稱之為變長對象;而浮點(diǎn)數(shù)所占的內(nèi)存都是一樣的,像這種內(nèi)存大小固定的對象,我們稱之為定長對象。
至于 Python 如何計(jì)算對象所占的內(nèi)存,我們在剖析具體對象的時(shí)候會說,因?yàn)檫@涉及到底層對應(yīng)的結(jié)構(gòu)體。
所以變長對象的特點(diǎn)是:同一個(gè)類型的實(shí)例對象,如果值不同,那么占用的內(nèi)存大小不同。像字符串、列表、元組、字典等,它們毫無疑問都是變長對象。值得一提的是,整數(shù)也是變長對象,因?yàn)?Python 整數(shù)的值在底層是通過數(shù)組維護(hù)的,后續(xù)介紹整數(shù)實(shí)現(xiàn)的時(shí)候再聊。
而定長對象的特點(diǎn)是:同一個(gè)類型的實(shí)例對象,不管值是多少,占用的內(nèi)存大小始終是固定的,比如浮點(diǎn)數(shù)。因?yàn)?Python 的浮點(diǎn)數(shù)的值在 C 中是通過一個(gè) double 來維護(hù)的。而 C 里面值的類型一旦確定,大小就不變了,所以 Python 浮點(diǎn)數(shù)的大小也是不變的。
但既然類型固定,大小固定,那么范圍肯定是有限的。所以當(dāng)浮點(diǎn)數(shù)不斷增大,會犧牲精度來進(jìn)行存儲。
圖片
如果實(shí)在過大,則拋出 OverFlowError。
圖片
當(dāng)然除了浮點(diǎn)數(shù)之外,布爾值、復(fù)數(shù)等也屬于定長對象,它們占用的內(nèi)存大小是固定的。
以上我們就分析了對象的種類,對象可以被分為可變對象和不可變對象,以及變長對象和定長對象。
本文參考自:
本文鏈接:http://www.www897cc.com/showinfo-26-87986-0.htmlPython 對象有哪幾種,我們可以從哪些角度進(jìn)行分類呢?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: 在Go語言中,這樣使用Json的