首頁>技術>

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  <class 'int'>>>> int('0123') 123>>>

因此可以用一張圖來描述面向物件在 Python 中的體現:

型別、物件體系

a 是一個整數(例項物件),其型別是 int (型別物件)。

>>> a = 123>>> a123>>> type(a)<class 'int'>>>> isinstance(a, int)True>>>

但是問題來了,按照面向物件的理論來說,物件是由類例項化得到的,這在 Python 中也是適用的。既然是物件,那麼就必定有一個類來例項化它,換句話說物件一定要有型別。至於一個物件的型別是什麼,就看這個物件是被誰例項化的,被誰例項化那麼型別就是誰。而我們說 Python 中一切皆物件,所以像 int、str、tuple 這些內建的型別也是具有相應的型別的,那麼它們的型別又是誰呢?

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

>>> type(int)<class 'type'>>>> type(str)<class 'type'>>>> type(dict)<class 'type'>>>> type(type)<class '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>>>

因此,綜合以上關係,我們可以得到下面這張關係圖:

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

class Female:    passprint(type(Female))  # <class 'type'>print(issubclass(Female, object))  # True

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

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

class Female:    passclass Girl(Female):    pass# 自定義類的型別都是typeprint(type(Girl))  # <class 'type'># 但Girl繼承自Female, 所以它是Female的子類print(issubclass(Girl, Female))  # True# 而Female繼承自object, 所以Girl也是object的子類print(issubclass(Girl, object))  # True# 這裡需要額外多提一句例項物件, 我們之前使用type得到的都是該類的型別物件# 換句話說誰例項化得到的它, 那麼對它使用type得到的就是誰print(type(Girl()))  # <class '__main__.Girl'>print(type(Female()))  # <class '__main__.Female'># 但是我們說Girl的父類是Female, Female的父類是object# 所以Girl的例項物件也是Female和object的例項物件, Female的例項物件也是object的例項物件print(isinstance(Girl(), Female))  # Trueprint(isinstance(Girl(), object))  # True

因此上面那張關係圖就可以變成下面這樣:

我們說可以使用 type 和__class__檢視一個物件的型別,並且還可以透過 isinstance 來判斷該物件是不是某個已知型別的例項物件;那如果想檢視一個型別物件都繼承了哪些類該怎麼做呢?我們目前都是使用 issubclass 來判斷某個型別物件是不是另一個已知型別物件的子類,那麼可不可以直接獲取某個型別物件都繼承了哪些類呢?

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

class A: passclass B: passclass C(A): passclass D(B, C): pass# 首先D繼承自B和C, C又繼承A, 我們現在要來檢視D繼承的父類# 方法一: 使用__base__print(D.__base__)  # <class '__main__.B'># 方法二: 使用__bases__print(D.__bases__)  # (<class '__main__.B'>, <class '__main__.C'>)# 方法三: 使用__mro__print(D.__mro__)# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
__base__: 如果繼承了多個類, 那麼只顯示繼承的第一個類, 沒有顯示繼承則返回一個\;__bases__: 返回一個元組, 會顯示所有直接繼承的父類, 如果沒有顯示的繼承, 則返回(\,);__mro__: mro 表示 Method Resolution Order, 表示方法查詢順序, 會從自身除法, 找到最頂層的父類, 因此返回自身、繼承的基類、以及基類繼承的基類, 一直找到 object;

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

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

>>> type.__base__<class 'object'>>>> object.__class__<class 'type'>>>>

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

總之目前記住兩點:

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

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

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

因此上面這種圖才算是完整,其實只看這張圖我們就能解讀出很多資訊。比如:例項物件的型別是型別物件,型別物件的型別是元類;所有的型別物件的基類都收斂於 object,所有物件的型別都收斂於 type。因此 Python 算是將一切皆物件的理念貫徹到了極致,也正因為如此,Python 才具有如此優秀的動態特性。

事實上,目前介紹的有些基礎了,但 Python 中的物件的概念確實非常重要。為了後面再分析原始碼的時候能夠更輕鬆,因此我們有必要系統地回顧一下,並且上面的關係圖會使我們在後面的學習變得輕鬆。因為等到看直譯器的時候,我們可就沒完了,就不那麼輕鬆了(なん~~~てね)。

Python中的變數只是個名字

Python 中的變數只是個名字,站在 C 語言的角度來說的話,Python 中的變數儲存的只是物件的記憶體地址,或者說指標,這個指標指向的記憶體儲存的才是物件。

所以在 Python 中,我們都說變數指向了某個物件。在其它靜態語言中,變數相當於是為某塊記憶體起的別名,獲取變數等於獲取這塊記憶體所儲存的值。而 Python 中變數代表的記憶體儲存的不是物件,只是物件的指標。

我們用兩段程式碼,一段 C 語言的程式碼,一段 Python 的程式碼,來看一下差別。

#include <stdio.h>void main(){    int a = 123;    printf("address of a = %p\n", &a);    a = 456    printf("address of a = %p\n", &a);}// 輸出結果/*address of a = 0x7fffa94de03caddress of a = 0x7fffa94de03c*/

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

a = 666print(hex(id(a)))  # 0x1b1333394f0a = 667print(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 中是如何體現的。

a = 666print(hex(id(a)))  # 0x1e6c51e3cf0b = aprint(hex(id(b)))  # 0x1e6c51e3cf0

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

我們說 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操作時,前後地址發生了變化,所以整型不支援本地修改,因此是一個不可變物件;

原來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 * 型別的陣列的首個元素的地址。

結構圖如下:

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

為什麼要這麼做?

因為在 Python 中一個物件一旦被建立,那麼它在記憶體中的大小就不可以變了。所以這就意味著那些可以容納可變長度資料的可變物件,要在內部維護一個指向可變大小的記憶體區域的指標。而我們看到 PyListObject 正是這麼做的,指標陣列的長度、記憶體大小是可變的,所以 PyListObject 內部並沒有直接儲存它,而是儲存了指向它的二級指標。但是 Python 在計算記憶體大小的時候是會將這個指標陣列也算進去的,所以 Python 中列表的大小是可變的,但是底層對應的 PyListObject 例項的大小是不變的,因為可變長度的指標陣列沒有存在 PyListObject 裡面。但為什麼要這麼設計呢?

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

定長物件與變長物件

Python 中一個物件佔用的記憶體有多大呢?相同型別的例項物件的大小是否相同呢?試一下就知道了,我們可以透過 sys 模組中 getsizeof 函式檢視一個物件所佔的記憶體。

import sysprint(sys.getsizeof(0))  # 24print(sys.getsizeof(1))  # 28print(sys.getsizeof(2 << 33))  # 32print(sys.getsizeof(0.))  # 24print(sys.getsizeof(3.14))  # 24print(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 "<stdin>", line 1, in <module>OverflowError: (34, 'Result too large')>>>

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

import sysprint(sys.getsizeof("a"))  # 50print(sys.getsizeof("abc"))  # 52

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

import sysprint(sys.getsizeof(""))  # 49

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

小結

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

名字空間分為:全域性名字空間(儲存全域性變數)、區域性名字空間(儲存區域性變數)、閉包名字空間(儲存閉包變數)、內建名字空間(儲存內建變數, 比如 int、str, 它們都在這裡),而名字空間又分為靜態名字空間和動態名字空間:比如區域性名字空間,因為函式中的區域性變數在編譯的時候就可以確定,所以函式對應的區域性名字空間使用一個數組儲存;而全域性變數在執行時可以進行動態新增、刪除,因此全域性名字空間使用的是一個字典來儲存,字典的 key 就是變數的名字(依舊是個指標,底層是指向字串(PyUnicodeObject)的指標),字典的 value 就是變數指向的物件的指標(或者說變數本身)。

a = 123b = "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 作用域的靈魂,它嚴格限制了變數的活動範圍,當然這些後面都會慢慢的說,因為飯要一口一口吃。因此這一節算是回顧基礎吧,雖說是基礎但是其實也涉及到了一些直譯器的知識,不過這一關我們遲早是要過的,所以就提前接觸一下吧。

原文連結:https://www.cnblogs.com/traditional/p/13391098.html

文章轉自:Python開發者

本文連結:http://www.yunweipai.com/39426.html

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 「Python資料處理」3.1