天天看點

python根據字元串執行個體化對象_解密 Python 中的對象模型

作者:古明地盆

來源: https://www.cnblogs.com/traditional/p/13391098.html

Python中一切皆對象

關于 Python,你肯定聽過這麼一句話:"Python中一切皆對象"。沒錯,在 Python 的世界裡,一切都是對象。

整型是一個對象、字元串是一個對象、字典是一個對象,甚至 int、str、list 等等,再加上我們使用 class 自定義的類,它們也是對象。

像 int、str、list 等基本類型,以及我們自定義的類,由于它們可以表示類型,是以我們稱之為類型對象;類型對象執行個體化得到的對象,我們稱之為執行個體對象。不管是哪種對象,它們都屬于對象。

是以 Python 中面向對象的理念貫徹的非常徹底,面向對象中的"類"和"對象"在 Python 中都是通過"對象"實作的。

在面向對象理論中,存在着"類"和"對象"兩個概念,像 int、dict、tuple、以及使用 class 關鍵字自定義的類型對象實作了面向對象理論中"類"的概念,而 123、(1, 2, 3),"xxx" 等等這些執行個體對象則實作了面向對象理論中"對象"的概念。但是在 Python 中,面向對象的"類"和"對象"都是通過對象實作的。

我們舉個栗子:

>>> # int它是一個類,是以它屬于類型對象, 類型對象執行個體化得到的對象屬于執行個體對象

>>> int

>>> int('0123')

123

>>>

是以可以用一張圖來描述面向對象在 Python 中的展現:

python根據字元串執行個體化對象_解密 Python 中的對象模型

類型、對象體系

a 是一個整數(執行個體對象),其類型是 int (類型對象)。

>>> a = 123

>>> a

123

>>> type(a)

>>> isinstance(a, int)

True

>>>

但是問題來了,按照面向對象的理論來說,對象是由類執行個體化得到的,這在 Python 中也是适用的。既然是對象,那麼就必定有一個類來執行個體化它,換句話說對象一定要有類型。至于一個對象的類型是什麼,就看這個對象是被誰執行個體化的,被誰執行個體化那麼類型就是誰。而我們說 Python 中一切皆對象,是以像 int、str、tuple 這些内置的類型也是具有相應的類型的,那麼它們的類型又是誰呢?

我們使用 type 函數檢視一下就好了。

>>> type(int)

>>> type(str)

>>> type(dict)

>>> type(type)

>>>

我們看到類型對象的類型,無一例外都是 type。type 應該是初學 Python 的時候就接觸了,當時使用 type 都是為了檢視一個對象的類型,然而 type 的作用遠沒有這麼簡單,我們後面會說,總之我們目前看到類型對象的類型是 type。

是以 int、str 等類型對象是 type 的對象,而 type 我們也稱其為元類,表示類型對象的類型。至于 type 本身,它的類型還是 type,是以它連自己都沒放過,把自己都變成自己的對象了。

是以在 Python 中,你能看到的任何對象都是有類型的,我們可以使用 type 函數檢視,也可以擷取該對象的__class__屬性檢視。 是以:執行個體對象、類型對象、元類,Python 中任何一個對象都逃不過這三種身份。

Python 中還有一個特殊的類型(對象),叫做 object,它是所有類型對象的基類。不管是什麼類,内置的類也好,我們自定義的類也罷,它們都繼承自 object。是以, object 是所有類型對象的"基類"、或者說"父類"。

>>> issubclass(int, object)

True

>>>

是以,綜合以上關系,我們可以得到下面這張關系圖:

python根據字元串執行個體化對象_解密 Python 中的對象模型

我們自定義的類型也是如此,舉個栗子:

class Female:

pass

print(type(Female)) #

print(issubclass(Female, object)) # True

在 Python3 中,自定義的類即使不顯式的繼承 object,也會預設繼承自 object。

python根據字元串執行個體化對象_解密 Python 中的對象模型

那麼我們自定義再自定義一個子類,繼承自 Female 呢?

class Female:

pass

class Girl(Female):

pass

# 自定義類的類型都是type

print(type(Girl)) #

# 但Girl繼承自Female, 是以它是Female的子類

print(issubclass(Girl, Female)) # True

# 而Female繼承自object, 是以Girl也是object的子類

print(issubclass(Girl, object)) # True

# 這裡需要額外多提一句執行個體對象, 我們之前使用type得到的都是該類的類型對象

# 換句話說誰執行個體化得到的它, 那麼對它使用type得到的就是誰

print(type(Girl())) #

print(type(Female())) #

# 但是我們說Girl的父類是Female, Female的父類是object

# 是以Girl的執行個體對象也是Female和object的執行個體對象, Female的執行個體對象也是object的執行個體對象

print(isinstance(Girl(), Female)) # True

print(isinstance(Girl(), object)) # True

是以上面那張關系圖就可以變成下面這樣:

python根據字元串執行個體化對象_解密 Python 中的對象模型

我們說可以使用 type 和__class__檢視一個對象的類型,并且還可以通過 isinstance 來判斷該對象是不是某個已知類型的執行個體對象;那如果想檢視一個類型對象都繼承了哪些類該怎麼做呢?我們目前都是使用 issubclass 來判斷某個類型對象是不是另一個已知類型對象的子類,那麼可不可以直接擷取某個類型對象都繼承了哪些類呢?

答案是可以的,方法有三種,我們分别來看一下:

class A: pass

class B: pass

class C(A): pass

class D(B, C): pass

# 首先D繼承自B和C, C又繼承A, 我們現在要來檢視D繼承的父類

# 方法一: 使用__base__

print(D.__base__) #

# 方法二: 使用__bases__

print(D.__bases__) # (, )

# 方法三: 使用__mro__

print(D.__mro__)

# (, , , , )

__base__: 如果繼承了多個類, 那麼隻顯示繼承的第一個類, 沒有顯示繼承則傳回一個;

__bases__: 傳回一個元組, 會顯示所有直接繼承的父類, 如果沒有顯示的繼承, 則傳回(,);

__mro__: mro 表示 Method Resolution Order, 表示方法查找順序, 會從自身除法, 找到最頂層的父類, 是以傳回自身、繼承的基類、以及基類繼承的基類, 一直找到 object;

最後我們來看一下 type 和 object,估計這兩個老鐵之間的關系會讓很多人感到困惑。

我們說 type 是所有類的元類,而 object 是所有的基類,這就說明 type 是要繼承自 object 的,而 object 的類型是 type。

>>> type.__base__

>>> object.__class__

>>>

這就怪了,這難道不是一個先有雞還是先有蛋的問題嗎?其實不是的,這兩個對象是共存的,它們之間的定義其實是互相依賴的。至于到底是怎麼肥事,我們後面在看解釋器源碼的時候就會很清晰了。

總之目前記住兩點:

type 站在類型金字塔的最頂端, 任何的對象按照類型追根溯源, 最終得到的都是 type;

object 站在繼承金字塔的最頂端, 任何的類型對象按照繼承追根溯源, 最終得到的都是 object;

我們說 type 的類型還是 type,但是 object 的基類則不再是 object,而是一個 None。為什麼呢?其實答案很簡單,我們說 Python 在查找屬性或方法的時候,會回溯繼承鍊,自身如果沒有的話,就會按照__mro__指定的順序去基類中查找。是以繼承鍊一定會有一個終點,否則就會像沒有出口的遞歸一樣出現死循環了。

最後将上面那張關系圖再完善一下的話:

python根據字元串執行個體化對象_解密 Python 中的對象模型

是以上面這種圖才算是完整,其實隻看這張圖我們就能解讀出很多資訊。比如:執行個體對象的類型是類型對象,類型對象的類型是元類;所有的類型對象的基類都收斂于 object,所有對象的類型都收斂于 type。是以 Python 算是将一切皆對象的理念貫徹到了極緻,也正因為如此,Python 才具有如此優秀的動态特性。

事實上,目前介紹的有些基礎了,但 Python 中的對象的概念确實非常重要。為了後面再分析源碼的時候能夠更輕松,是以我們有必要系統地回顧一下,并且上面的關系圖會使我們在後面的學習變得輕松。因為等到看解釋器的時候,我們可就沒完了,就不那麼輕松了(なん~~~てね)。Python中的變量隻是個名字

Python 中的變量隻是個名字,站在 C 語言的角度來說的話,Python 中的變量存儲的隻是對象的記憶體位址,或者說指針,這個指針指向的記憶體存儲的才是對象。

是以在 Python 中,我們都說變量指向了某個對象。在其它靜态語言中,變量相當于是為某塊記憶體起的别名,擷取變量等于擷取這塊記憶體所存儲的值。而 Python 中變量代表的記憶體存儲的不是對象,隻是對象的指針。

我們用兩段代碼,一段 C 語言的代碼,一段 Python 的代碼,來看一下差别。

#include

void main()

{

int a = 123;

printf("address of a = %p\n", &a);

a = 456

printf("address of a = %p\n", &a);

}

// 輸出結果

我們看到前後輸出的位址是一樣的,再來看看 Python 的。

a = 666

print(hex(id(a))) # 0x1b1333394f0

a = 667

print(hex(id(a))) # 0x1b133339510

然而我們看到 Python 中變量 a 的位址前後發生了變化,我們分析一下原因。

首先在 C 中,建立一個變量的時候必須規定好類型,比如 int a = 666,那麼變量 a 就是 int 類型,以後在所處的作用域中就不可以變了。如果這時候,再設定 a = 777,那麼等于是把記憶體中存儲的 666 換成 777,a 的位址和類型是不會變化的。

而在 Python 中,a = 666 等于是先開辟一塊記憶體,存儲的值為 666,然後讓變量 a 指向這片記憶體,或者說讓變量 a 存儲這塊記憶體的指針。然後 a = 777 的時候,再開辟一塊記憶體,然後讓 a 指向存儲 777 的記憶體,由于是兩塊不同的記憶體,是以它們的位址是不一樣的。

python根據字元串執行個體化對象_解密 Python 中的對象模型

是以 Python 中的變量隻是一個和對象關聯的名字罷了,它代表的是對象的指針。換句話說 Python 中的變量就是個便利貼,可以貼在任何對象上,一旦貼上去了,就代表這個對象被引用了。

我們再來看看變量之間的傳遞,在 Python 中是如何展現的。

a = 666

print(hex(id(a))) # 0x1e6c51e3cf0

b = a

print(hex(id(b))) # 0x1e6c51e3cf0

我們看到列印的位址是一樣的,我們再用一張圖解釋一下。

python根據字元串執行個體化對象_解密 Python 中的對象模型

我們說 a = 666 的時候,先開辟一份記憶體,再讓 a 存儲對應記憶體的指針;然後 b = a 的時候,會把 a 的位址拷貝一份給 b,是以 b 存儲了和 a 相同的位址,它們都指向了同一個對象。

是以說 Python 是值傳遞、或者引用傳遞都是不準确的,準确的說 Python 是變量之間的指派傳遞,對象之間的引用傳遞。 因為 Python 中的變量本質上就是一個指針,是以在 b = a 的時候,等于把a的位址拷貝一份給b,是以對于變量來說是指派傳遞;然後 a 和 b 又都是指向對象的指針,是以對于對象來說是引用傳遞。

另外還有最關鍵的一點,我們說 Python 中的變量是一個指針,當傳遞一個變量的時候,傳遞的是指針;但是在操作一個變量的時候,會操作變量指向的記憶體。

是以 id(a) 擷取的不是 a 的位址,而是 a 指向的記憶體的位址(在底層其實就是a),同理 b = a,是将 a 本身,或者說将 a 存儲的、指向某個具體的對象的位址傳遞給了 b。

另外在 C 的層面上,a 和 b 屬于指針變量,那麼 a 和 b 有沒有位址呢?顯然是有的,隻不過在 Python 中你是看不到的,Python 解釋器隻允許你看到對象的位址。

最後提一下變量的類型

我們說變量的類型其實不是很準确,應該是變量指向(引用)的對象的類型,因為我們說 Python 中變量是個指針,操作指針會操作指針指向的記憶體,是以我們使用 type(a) 檢視的是變量 a 指向的記憶體的類型,當然為了友善也會直接說變量的類型,了解就行。那麼問題來了,我們在建立一個變量的時候,并沒有顯示的指定類型啊,但 Python 顯然是有類型的,那麼 Python 是如何判斷一個變量指向的是什麼類型的資料呢?

答案是:解釋器是通過靠猜的方式,通過你賦的值(或者說變量引用的值)來推斷類型。是以在 Python 中,如果你想建立一個變量,那麼必須在建立變量的時候同時指派,否則解釋器就不知道這個變量指向的資料是什麼類型。是以 Python 是先建立相應的值,這個值在 C 中對應一個結構體,結構體裡面有一個成員專門用來存儲該值對應的類型。當建立完值之後,再讓這個變量指向它,是以 Python 中是先有值後有變量。但顯然 C 中不是這樣的,因為 C 中變量代表的記憶體所存儲的就是具體的值,是以 C 中可以直接聲明一個變量的同時不指派。因為 C 要求聲明變量的同時必須指定類型,是以聲明變量的同時,其類型和記憶體大小就已經固定了。而 Python 中變量代表的記憶體是個指針,它隻是指向了某個對象,是以由于其便利貼的特性,可以貼在任意對象上面,但是不管貼在哪個對象,你都必須先有對象才可以,不然變量貼誰去?

另外,盡管 Python 在建立變量的時候不需要指定類型,但 Python 是強類型語言,強類型語言,強類型語言,重要的事情說三遍。而且是動态強類型,因為類型的強弱和是否需要顯示聲明類型之間沒有關系。

可變對象與不可變對象

我們說一個對象其實就是一片被配置設定的記憶體空間,記憶體中存儲了相應的值,不過這些空間可以是連續的,也可以是不連續的。

不可變對象一旦建立,其記憶體中存儲的值就不可以再修改了。如果想修改,隻能建立一個新的對象,然後讓變量指向新的對象,是以前後的位址會發生改變。而可變對象在建立之後,其存儲的值可以動态修改。

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

>>> a = 666

>>> id(a)

1365442984464

>>> a += 1

>>> id(a)

1365444032848

>>>

我們看到在對 a 執行+1操作時,前後位址發生了變化,是以整型不支援本地修改,是以是一個不可變對象;

python根據字元串執行個體化對象_解密 Python 中的對象模型

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

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

可能有人覺得,每次都要建立新對象,銷毀舊對象,效率肯定會很低吧。事實上确實如此,但是後面我們會從源碼的角度上來看 Python 如何通過小整數對象池等手段進行優化。

而清單是一個可變對象,它是可以修改的。

這裡先多提一句,Python中的對象本質上就是C中malloc函數為結構體執行個體在堆區申請的一塊記憶體。Python中的任何對象在C中都會對應一個結構體,這個結構體除了存放具體的值之外,還存放了一些額外的資訊,這個我們在剖析Python中的内置類型的執行個體對象的時候會細說。

首先Python中清單,當然不光是清單,還有元組、集合,這些容器它們的内部存儲的也不是具體的對象,而是對象的指針。比如:lst = [1, 2, 3],你以為lst存儲的是三個整型對象嗎?其實不是的,lst存儲的是三個整型對象的指針,當我們使用lst[0]的時候,拿到的是第一個元素的指針,但是操作(比如print)的時候會自動操作(print)指針指向的記憶體。

不知道你是否思考過,Python底層是C來實作的,是以Python中的清單的實作必然要借助C中的數組。可我們知道C中的數組裡面的所有元素的類型必須一緻,但清單卻可以存放任意的元素,是以從這個角度來講,清單裡面的元素它就就不可能是對象,因為不同的對象在底層對應的結構體是不同的,是以這個元素隻能是指針。

可能有人又好奇了,不同對象的指針也是不同的啊,是的,但C中的指針是可以轉化的。Python底層将所有對象的指針,都轉成了 PyObject 的指針,這樣不就是同一種類型的指針了嗎?關于這個PyObject,它是我們後面要剖析的重中之重,這個PyObject貫穿了我們的整個系列。目前隻需要知道Python中的清單存儲的值,在底層是通過一個 PyObject * 類型的資料來維護的。

>>> lst = [1, 2, 3]

>>> id(lst)

1365442893952

>>> lst.append(4)

>>> lst

[1, 2, 3, 4]

>>> id(lst)

1365442893952

>>>

我們看到清單在添加元素的時候,前後位址并沒有改變。清單在C中是通過PyListObject實作的,我們在介紹清單的時候會細說。這個PyListObject内部除了一些基本資訊之外,還有一個成員叫ob_item,它是一個PyObject的二級指針,指向了我們剛才說的 PyObject * 類型的數組的首個元素的位址。

結構圖如下:

python根據字元串執行個體化對象_解密 Python 中的對象模型

顯然圖中的指針數組是用來存儲具體的對象的指針的,每一個指針都指向了相應的對象(這裡是整型對象)。可能有人注意到,整型對象的順序有點怪,其實我是故意這麼畫的。因為 PyObject * 數組内部的元素是連續且有順序的,但是指向的整型對象則是存儲在堆區的,它們的位置是任意性的。但是不管這些整型對象存儲在堆區的什麼位置,它們和數組中的指針都是一一對應的,我們通過索引是可以正确擷取到指向的對象的。

另外我們還可以看到一個現象,那就是Python中的清單在底層是分開存儲的,因為PyListObject結構體執行個體并沒有存儲相應的指針數組,而是存儲了指向這個指針數組的二級指針。顯然我們添加、删除、修改元素等操作,都是通過這個二級指針來間接操作這個指針數組。

為什麼要這麼做?

因為在 Python 中一個對象一旦被建立,那麼它在記憶體中的大小就不可以變了。是以這就意味着那些可以容納可變長度資料的可變對象,要在内部維護一個指向可變大小的記憶體區域的指針。而我們看到 PyListObject 正是這麼做的,指針數組的長度、記憶體大小是可變的,是以 PyListObject 内部并沒有直接存儲它,而是存儲了指向它的二級指針。但是 Python 在計算記憶體大小的時候是會将這個指針數組也算進去的,是以 Python 中清單的大小是可變的,但是底層對應的 PyListObject 執行個體的大小是不變的,因為可變長度的指針數組沒有存在 PyListObject 裡面。但為什麼要這麼設計呢?

這麼做的原因就在于,遵循這樣的規則可以使通過指針維護對象的工作變得非常簡單。一旦允許對象的大小可在運作期改變,那麼我們就可以考慮如下場景。在記憶體中有對象A,并且其後面緊跟着對象B。如果運作的某個時候,A的大小增大了,這就意味着必須将A整個移動到記憶體中的其他位置,否則A增大的部分會覆寫掉原本屬于B的資料。隻要将A移動到記憶體的其他位置,那麼所有指向A的指針就必須立即得到更新。可想而知這樣的工作是多麼的繁瑣,而通過一個指針去操作就變得簡單多了。定長對象與變長對象

Python 中一個對象占用的記憶體有多大呢?相同類型的執行個體對象的大小是否相同呢?試一下就知道了,我們可以通過 sys 子產品中 getsizeof 函數檢視一個對象所占的記憶體。

import sys

print(sys.getsizeof(0)) # 24

print(sys.getsizeof(1)) # 28

print(sys.getsizeof(2 << 33)) # 32

print(sys.getsizeof(0.)) # 24

print(sys.getsizeof(3.14)) # 24

print(sys.getsizeof((2 << 33) + 3.14)) # 24

我們看到整型對象的大小不同,所占的記憶體也不同,像這種記憶體大小不固定的對象,我們稱之為變長對象;而浮點數所占的記憶體都是一樣的,像這種記憶體大小固定的對象,我們稱之為定長對象。

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

而且我們知道 Python 中的整數是不會溢出的,而C中的整型顯然是有最大範圍的,那麼Python是如何做到的呢?答案是Python在底層是通過C的32位整型數組來存儲自身的整型對象的,通過多個32位整型組合起來,以支援存儲更大的數值,是以整型越大,就需要越多的32位整數。而32位整數是4位元組,是以我們上面代碼中的那些整型,都是4位元組、4位元組的增長。

當然Python中的對象在底層都是一個結構體,這個結構體中除了維護具體的值之外,還有其它的成員資訊,在計算記憶體大小的時候,它們也是要考慮在内的,當然這些我們後面會說。

而浮點數的大小是不變的,因為Python的浮點數的值在C中是通過一個double來維護的。而C中的值的類型一旦确定,大小就不變了,是以Python的float也是不變的。

但是既然是固定的類型,肯定範圍是有限的,是以當浮點數不斷增大,會犧牲精度來進行存儲。如果實在過大,那麼會抛出OverFlowError。

>>> int(1000000000000000000000000000000000.) # 犧牲了精度

999999999999999945575230987042816

>>> 10 ** 1000 # 不會溢出

1000000000000000......

>>>

>>> 10. ** 1000 # 報錯了

Traceback (most recent call last):

File "", line 1, in

OverflowError: (34, 'Result too large')

>>>

還有字元串,字元串毫無疑問肯定是可變對象,因為長度不同大小不同。

import sys

print(sys.getsizeof("a")) # 50

print(sys.getsizeof("abc")) # 52

我們看到多了兩個字元,多了兩個位元組,這很好了解。但是這些說明了一個空字元串要占49個位元組,我們來看一下。

import sys

print(sys.getsizeof("")) # 49

顯然是的,顯然這 49 個位元組是用來維護其它成員資訊的,因為底層的結構體除了維護具體的值之外,還要維護其它的資訊,比如:引用計數等等,這些在分析源碼的時候會詳細說。

小結

我們這一節介紹了 Python 中的對象體系,我們說 Python 中一切皆對象,類型對象和執行個體對象都屬于對象;還說了對象的種類,根據是否支援本地修改可以分為可變對象和不可變對象,根據占用的記憶體是否不變可以分為定長對象和變長對象;還說了 Python 中變量的本質,Python 中的變量本質上是一個指針,而變量的名字則存儲在對應的名字空間(或者說命名空間)中,當然名字空間我們沒有說,是因為這些在後續系列會詳細說(又是後續, 不管咋樣, 坑先挖出來),不過這裡可以先補充一下。

名字空間分為:全局名字空間(存儲全局變量)、局部名字空間(存儲局部變量)、閉包名字空間(存儲閉包變量)、内建名字空間(存儲内置變量, 比如 int、str, 它們都在這裡),而名字空間又分為靜态名字空間和動态名字空間:比如局部名字空間,因為函數中的局部變量在編譯的時候就可以确定,是以函數對應的局部名字空間使用一個數組存儲;而全局變量在運作時可以進行動态添加、删除,是以全局名字空間使用的是一個字典來儲存,字典的 key 就是變量的名字(依舊是個指針,底層是指向字元串(PyUnicodeObject)的指針),字典的 value 就是變量指向的對象的指針(或者說變量本身)。

a = 123

b = "xxx"

# 通過globals()即可擷取全局名字空間

print(globals()) #{..., 'a': 123, 'b': 'xxx'}

# 我們看到雖然顯示的是變量名和變量指向的值

# 但是在底層,字典存儲的鍵值對也是指向具體對象的指針

# 隻不過我們說操作指針會操作指向的記憶體,是以這裡print列印之後,顯示的也是具體的值,但是存儲的是指針

# 至于對象本身,則存儲在堆區,并且被指針指向

# 此外,我們往全局名字空間中設定一個鍵值對,也等價于建立了一個全局變量

globals()["c"] = "hello"

print(c) # hello

# 此外這個全局名字空間是唯一的,即使你把它放在函數中也是一樣

def foo():

globals()["d"] = "古明地覺"

# foo一旦執行,{"d": "古明地覺"}就設定進了全局名字空間中

foo()

print(d) # 古明地覺

怎麼樣,是不是有點神奇呢?是以名字空間是 Python 作用域的靈魂,它嚴格限制了變量的活動範圍,當然這些後面都會慢慢的說,因為飯要一口一口吃。是以這一節算是回顧基礎吧,雖說是基礎但是其實也涉及到了一些解釋器的知識,不過這一關我們遲早是要過的,是以就提前接觸一下吧。