Python對象有哪幾種,我們可以從哪些角度進行分類呢?

程序員咋不禿頭 2024-05-15 00:20:58

楔子

在程序開發中,我們每時每刻都在創建對象,那到底什麽是對象呢?

其實一個對象就是一片被分配的內存空間,空間可以是連續的,也可以是不連續的。然後空間裏面存儲了指定的數據,並提供了操作數據的一些功能方法。而按照是否可變和內存大小是否固定,我們可以將對象進行如下分類。

可變對象和不可變對象;定長對象和變長對象;

下面來詳細解釋一下。

可變對象和不可變對象

不可變對象一旦創建,其內存中存儲的值就不可以再修改了。如果想修改,只能創建一個新的對象,然後讓變量指向新的對象,所以前後的地址會發生改變。而可變對象在創建之後,其存儲的值可以動態修改。

像整數就是一個不可變對象。

>>> a = 666>>> id(a)2230564873872>>> a += 1>>> id(a)2230564873808

我們看到執行 a += 1 操作之後,前後地址發生了變化,所以整數不支持本地修改,因此是一個不可變對象;

原來 a = 666,而我們說操作一個變量等于操作這個變量指向的內存,所以 a+=1 會將 a 指向的整數對象 666 和 1 進行加法運算,得到 667。因此會開辟新的空間來存儲 667,然後讓 a 指向這片新的空間。至于原來的 666 所占的空間怎麽辦,解釋器會看它的引用計數,如果不爲 0 代表還有變量引用(指向)它,如果爲 0 證明沒有變量引用了,所以會被回收。

關于引用計數,我們後面會詳細說,目前只需要知道當一個對象被一個變量引用的時候,那麽該對象的引用計數就會加 1。有幾個變量引用,那麽它的引用計數就是幾。

除了整數之外,浮點數、字符串、布爾值等等,都是不可變對象,它們的值不能本地修改。

然後是可變對象,像列表、字典、集合等都是可變對象,它們支持動態修改。

這裏先多提一句,Python 的對象本質上就是 C 中 malloc 函數爲結構體實例在堆區申請的一塊內存。Python 的任何對象在 C 中都會對應一個結構體,這個結構體除了存放具體的值之外,還存放了一些額外的信息,這個我們在後續剖析內置對象的時候會細說。

在上一篇文章中我們說到,列表、元組、集合這些容器的內部存儲的不是具體的對象,而是對象的指針。比如:lst = [1, 2, 3],你以爲列表存儲的是三個整數對象嗎?其實不是的,它存儲的是三個整數對象的指針,當我們使用 lst[0] 的時候,拿到的是一個指針,但是操作(比如 print)的時候會自動操作指針指向的內存。

因爲 Python 底層是 C 來實現的,所以列表的實現必然要借助 C 的數組。可 C 數組裏面的元素的類型必須一致,但列表卻可以存放任意的元素,因此從這個角度上講,列表裏面的元素就不可能是對象,因爲不同的對象在底層對應的結構體是不同的,所以元素只能是指針。

可能有人又好奇了,不同對象的指針也是不同的啊,是的,但 C 指針是可以轉化的。Python 底層將所有對象的指針,都轉成了 PyObject 類型的指針,這樣不就是同一種類型的指針了嗎?關于這個 PyObject,它是我們後面要剖析的重中之重,貫穿了整個系列。不過目前只需要知道列表(還有其它容器)存儲的元素、以及 Python 的變量,它們都是一個泛型指針 PyObject *。

>>> lst = [1, 2, 3]>>> id(lst)2287192570048>>> lst.append(4)>>> lst[1, 2, 3, 4]>>> id(lst)2287192570048

我們看到列表在添加元素的時候,前後地址並沒有改變。列表在 C 中是通過 PyListObject 結構體實現的,我們在介紹列表的時候會細說。這個 PyListObject 內部除了一些基本信息之外,還維護了一個 PyObject 的二級指針,指向了 PyObject * 類型的數組的首元素。

顯然圖中的指針數組用來存儲具體的對象的指針,每一個指針都指向了相應的對象(這裏是整數對象)。

然後我們還可以看到一個現象,那就是列表在底層是分開存儲的,因爲 PyListObject 結構體實例並沒有存儲相應的指針數組,而是存儲了一個二級指針。顯然添加、刪除、修改元素等操作,都是通過這個二級指針來間接操作指針數組。

因爲一個對象一旦被創建(任何語言都是如此),那麽它在內存中的大小就不可以變了。所以這就意味著那些可以容納可變長度數據的可變對象,要在內部維護一個指針,指針指向一片內存區域,該區域存放具體的數據。如果空間不夠了,那就申請一片更大的內存區域,然後將元素依次拷貝過去,再讓指針指向新的內存區域。而列表的底層也是這麽做的,其內部並沒有直接存儲具體的指針數組,而是存儲了指向指針數組首元素的二級指針。

那麽問題來了,爲什麽要這麽做?

其實很好理解,遵循這樣的規則可以使通過指針維護對象的工作變得非常簡單。一旦允許對象的大小可在運行期改變,那麽我們就要考慮如下場景。

在內存中有對象 A,並且其後面緊跟著對象 B。如果在運行的某個時候,A 的大小增大了,這就意味著必須將 A 整個移動到內存中的其他位置,否則 A 增大的部分會覆蓋掉原本屬于 B 的數據。但要將 A 移動到內存的其他位置,那麽所有指向 A 的指針就必須立即得到更新。可想而知這樣的工作是多麽的繁瑣,因此通過在可變對象的內部維護一個指針就變得簡單多了。

定長對象和變長對象

所謂定長和變長,取決于對象所占的內存大小是否固定,舉個例子。

>>> 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

我們看到字符串的長度不同,所占的內存也不同,像這種內存大小不固定的對象,我們稱之爲變長對象;而浮點數所占的內存都是一樣的,像這種內存大小固定的對象,我們稱之爲定長對象。

至于 Python 如何計算對象所占的內存,我們在剖析具體對象的時候會說,因爲這涉及到底層對應的結構體。

所以變長對象的特點是:同一個類型的實例對象,如果值不同,那麽占用的內存大小不同。像字符串、列表、元組、字典等,它們毫無疑問都是變長對象。值得一提的是,整數也是變長對象,因爲 Python 整數的值在底層是通過數組維護的,後續介紹整數實現的時候再聊。

而定長對象的特點是:同一個類型的實例對象,不管值是多少,占用的內存大小始終是固定的,比如浮點數。因爲 Python 的浮點數的值在 C 中是通過一個 double 來維護的。而 C 裏面值的類型一旦確定,大小就不變了,所以 Python 浮點數的大小也是不變的。

但既然類型固定,大小固定,那麽範圍肯定是有限的。所以當浮點數不斷增大,會犧牲精度來進行存儲。

如果實在過大,則抛出 OverFlowError。

當然除了浮點數之外,布爾值、複數等也屬于定長對象,它們占用的內存大小是固定的。

小結

以上我們就分析了對象的種類,對象可以被分爲可變對象和不可變對象,以及變長對象和定長對象。

不可變對象:對象不支持本地修改;可變對象:對象支持本地修改;變長對象:對象維護的值不同,占用的內存大小也不同;定長對象:占用的內存大小始終固定;
0 阅读:16

程序員咋不禿頭

簡介:感謝大家的關注