解密 Python 中的對象模型
Python中一切皆對象
關(guān)于 Python,你肯定聽過這么一句話:"Python中一切皆對象"。沒錯,在 Python 的世界里,一切都是對象。
整型是一個對象、字符串是一個對象、字典是一個對象,甚至 int、str、list 等等,再加上我們使用 class 自定義的類,它們也是對象。
像 int、str、list 等基本類型,以及我們自定義的類,由于它們可以表示類型,因此我們稱之為類型對象;類型對象實例化得到的對象,我們稱之為實例對象。不管是哪種對象,它們都屬于對象。
因此 Python 中面向?qū)ο蟮睦砟钬瀼氐姆浅氐?,面向?qū)ο笾械?類"和"對象"在 Python 中都是通過"對象"實現(xiàn)的。
在面向?qū)ο罄碚撝?,存在?類"和"對象"兩個概念,像 int、dict、tuple、以及使用 class 關(guān)鍵字自定義的類型對象實現(xiàn)了面向?qū)ο罄碚撝?類"的概念,而 123、(1, 2, 3),"xxx" 等等這些實例對象則實現(xiàn)了面向?qū)ο罄碚撝?對象"的概念。但是在 Python 中,面向?qū)ο蟮?類"和"對象"都是通過對象實現(xiàn)的。
我們舉個栗子:
>>>?#?int它是一個類,因此它屬于類型對象,?類型對象實例化得到的對象屬于實例對象
>>>?int??
<class?'int'>
>>>?int('0123')?
123
>>>
因此可以用一張圖來描述面向?qū)ο笤?Python 中的體現(xiàn):

類型、對象體系
a 是一個整數(shù)(實例對象),其類型是 int (類型對象)。
>>>?a?=?123
>>>?a
123
>>>?type(a)
<class?'int'>
>>>?isinstance(a,?int)
True
>>>
但是問題來了,按照面向?qū)ο蟮睦碚搧碚f,對象是由類實例化得到的,這在 Python 中也是適用的。既然是對象,那么就必定有一個類來實例化它,換句話說對象一定要有類型。至于一個對象的類型是什么,就看這個對象是被誰實例化的,被誰實例化那么類型就是誰。而我們說 Python 中一切皆對象,所以像 int、str、tuple 這些內(nèi)置的類型也是具有相應(yīng)的類型的,那么它們的類型又是誰呢?
我們使用 type 函數(shù)查看一下就好了。
>>>?type(int)
<class?'type'>
>>>?type(str)
<class?'type'>
>>>?type(dict)
<class?'type'>
>>>?type(type)
<class?'type'>
>>>
我們看到類型對象的類型,無一例外都是 type。type 應(yīng)該是初學(xué) Python 的時候就接觸了,當(dāng)時使用 type 都是為了查看一個對象的類型,然而 type 的作用遠沒有這么簡單,我們后面會說,總之我們目前看到類型對象的類型是 type。
所以 int、str 等類型對象是 type 的對象,而 type 我們也稱其為元類,表示類型對象的類型。至于 type 本身,它的類型還是 type,所以它連自己都沒放過,把自己都變成自己的對象了。
因此在 Python 中,你能看到的任何對象都是有類型的,我們可以使用 type 函數(shù)查看,也可以獲取該對象的__class__屬性查看。
所以:實例對象、類型對象、元類,Python 中任何一個對象都逃不過這三種身份。
Python 中還有一個特殊的類型(對象),叫做 object,它是所有類型對象的基類。不管是什么類,內(nèi)置的類也好,我們自定義的類也罷,它們都繼承自 object。因此, object 是所有類型對象的"基類"、或者說"父類"。
>>>?issubclass(int,?object)
True
>>>
因此,綜合以上關(guān)系,我們可以得到下面這張關(guān)系圖:

我們自定義的類型也是如此,舉個栗子:
class?Female:
????pass
print(type(Female))??#?
print(issubclass(Female,?object))??#?True
在 Python3 中,自定義的類即使不顯式的繼承 object,也會默認繼承自 object。

那么我們自定義再自定義一個子類,繼承自 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
因此上面那張關(guān)系圖就可以變成下面這樣:

我們說可以使用 type 和__class__查看一個對象的類型,并且還可以通過 isinstance 來判斷該對象是不是某個已知類型的實例對象;那如果想查看一個類型對象都繼承了哪些類該怎么做呢?我們目前都是使用 issubclass 來判斷某個類型對象是不是另一個已知類型對象的子類,那么可不可以直接獲取某個類型對象都繼承了哪些類呢?
答案是可以的,方法有三種,我們分別來看一下:
class?A:?pass
class?B:?pass
class?C(A):?pass
class?D(B,?C):?pass
#?首先D繼承自B和C,?C又繼承A,?我們現(xiàn)在要來查看D繼承的父類
#?方法一:?使用__base__
print(D.__base__)??#?
#?方法二:?使用__bases__
print(D.__bases__)??#?(,?)
#?方法三:?使用__mro__
print(D.__mro__)
#?(,?,?,?,?)
__base__: 如果繼承了多個類, 那么只顯示繼承的第一個類, 沒有顯示繼承則返回一個
; __bases__: 返回一個元組, 會顯示所有直接繼承的父類, 如果沒有顯示的繼承, 則返回(
,); __mro__: mro 表示 Method Resolution Order, 表示方法查找順序, 會從自身除法, 找到最頂層的父類, 因此返回自身、繼承的基類、以及基類繼承的基類, 一直找到 object;
最后我們來看一下 type 和 object,估計這兩個老鐵之間的關(guān)系會讓很多人感到困惑。
我們說 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__指定的順序去基類中查找。所以繼承鏈一定會有一個終點,否則就會像沒有出口的遞歸一樣出現(xiàn)死循環(huán)了。
最后將上面那張關(guān)系圖再完善一下的話:

因此上面這種圖才算是完整,其實只看這張圖我們就能解讀出很多信息。比如:實例對象的類型是類型對象,類型對象的類型是元類;所有的類型對象的基類都收斂于 object,所有對象的類型都收斂于 type。因此 Python 算是將一切皆對象的理念貫徹到了極致,也正因為如此,Python 才具有如此優(yōu)秀的動態(tài)特性。
事實上,目前介紹的有些基礎(chǔ)了,但 Python 中的對象的概念確實非常重要。為了后面再分析源碼的時候能夠更輕松,因此我們有必要系統(tǒng)地回顧一下,并且上面的關(guān)系圖會使我們在后面的學(xué)習(xí)變得輕松。因為等到看解釋器的時候,我們可就沒完了,就不那么輕松了(なん~~~てね)。
Python中的變量只是個名字
Python 中的變量只是個名字,站在 C 語言的角度來說的話,Python 中的變量存儲的只是對象的內(nèi)存地址,或者說指針,這個指針指向的內(nèi)存存儲的才是對象。
所以在 Python 中,我們都說變量指向了某個對象。在其它靜態(tài)語言中,變量相當(dāng)于是為某塊內(nèi)存起的別名,獲取變量等于獲取這塊內(nèi)存所存儲的值。而 Python 中變量代表的內(nèi)存存儲的不是對象,只是對象的指針。
我們用兩段代碼,一段 C 語言的代碼,一段 Python 的代碼,來看一下差別。
#include?
void?main()
{
????int?a?=?123;
????printf("address?of?a?=?%p\n",?&a);
????a?=?456
????printf("address?of?a?=?%p\n",?&a);
}
//?輸出結(jié)果
/*
address?of?a?=?0x7fffa94de03c
address?of?a?=?0x7fffa94de03c
*/
我們看到前后輸出的地址是一樣的,再來看看 Python 的。
a?=?666
print(hex(id(a)))??#?0x1b1333394f0
a?=?667
print(hex(id(a)))??#?0x1b133339510
然而我們看到 Python 中變量 a 的地址前后發(fā)生了變化,我們分析一下原因。
首先在 C 中,創(chuàng)建一個變量的時候必須規(guī)定好類型,比如 int a = 666,那么變量 a 就是 int 類型,以后在所處的作用域中就不可以變了。如果這時候,再設(shè)置 a = 777,那么等于是把內(nèi)存中存儲的 666 換成 777,a 的地址和類型是不會變化的。
而在 Python 中,a = 666 等于是先開辟一塊內(nèi)存,存儲的值為 666,然后讓變量 a 指向這片內(nèi)存,或者說讓變量 a 存儲這塊內(nèi)存的指針。然后 a = 777 的時候,再開辟一塊內(nèi)存,然后讓 a 指向存儲 777 的內(nèi)存,由于是兩塊不同的內(nèi)存,所以它們的地址是不一樣的。

所以 Python 中的變量只是一個和對象關(guān)聯(lián)的名字罷了,它代表的是對象的指針。換句話說 Python 中的變量就是個便利貼,可以貼在任何對象上,一旦貼上去了,就代表這個對象被引用了。
我們再來看看變量之間的傳遞,在 Python 中是如何體現(xiàn)的。
a?=?666
print(hex(id(a)))??#?0x1e6c51e3cf0
b?=?a
print(hex(id(b)))??#?0x1e6c51e3cf0
我們看到打印的地址是一樣的,我們再用一張圖解釋一下。

我們說 a = 666 的時候,先開辟一份內(nèi)存,再讓 a 存儲對應(yīng)內(nèi)存的指針;然后 b = a 的時候,會把 a 的地址拷貝一份給 b,所以 b 存儲了和 a 相同的地址,它們都指向了同一個對象。
因此說 Python 是值傳遞、或者引用傳遞都是不準確的,準確的說 Python 是變量之間的賦值傳遞,對象之間的引用傳遞。
因為 Python 中的變量本質(zhì)上就是一個指針,所以在 b = a 的時候,等于把a的地址拷貝一份給b,所以對于變量來說是賦值傳遞;然后 a 和 b 又都是指向?qū)ο蟮闹羔?,因此對于對象來說是引用傳遞。
另外還有最關(guān)鍵的一點,我們說 Python 中的變量是一個指針,當(dāng)傳遞一個變量的時候,傳遞的是指針;但是在操作一個變量的時候,會操作變量指向的內(nèi)存。
所以 id(a) 獲取的不是 a 的地址,而是 a 指向的內(nèi)存的地址(在底層其實就是a),同理 b = a,是將 a 本身,或者說將 a 存儲的、指向某個具體的對象的地址傳遞給了 b。
另外在 C 的層面上,a 和 b 屬于指針變量,那么 a 和 b 有沒有地址呢?顯然是有的,只不過在 Python 中你是看不到的,Python 解釋器只允許你看到對象的地址。
最后提一下變量的類型
我們說變量的類型其實不是很準確,應(yīng)該是變量指向(引用)的對象的類型,因為我們說 Python 中變量是個指針,操作指針會操作指針指向的內(nèi)存,所以我們使用 type(a) 查看的是變量 a 指向的內(nèi)存的類型,當(dāng)然為了方便也會直接說變量的類型,理解就行。那么問題來了,我們在創(chuàng)建一個變量的時候,并沒有顯示的指定類型啊,但 Python 顯然是有類型的,那么 Python 是如何判斷一個變量指向的是什么類型的數(shù)據(jù)呢?
答案是:解釋器是通過靠猜的方式,通過你賦的值(或者說變量引用的值)來推斷類型。所以在 Python 中,如果你想創(chuàng)建一個變量,那么必須在創(chuàng)建變量的時候同時賦值,否則解釋器就不知道這個變量指向的數(shù)據(jù)是什么類型。所以 Python 是先創(chuàng)建相應(yīng)的值,這個值在 C 中對應(yīng)一個結(jié)構(gòu)體,結(jié)構(gòu)體里面有一個成員專門用來存儲該值對應(yīng)的類型。當(dāng)創(chuàng)建完值之后,再讓這個變量指向它,所以 Python 中是先有值后有變量。但顯然 C 中不是這樣的,因為 C 中變量代表的內(nèi)存所存儲的就是具體的值,所以 C 中可以直接聲明一個變量的同時不賦值。因為 C 要求聲明變量的同時必須指定類型,所以聲明變量的同時,其類型和內(nèi)存大小就已經(jīng)固定了。而 Python 中變量代表的內(nèi)存是個指針,它只是指向了某個對象,所以由于其便利貼的特性,可以貼在任意對象上面,但是不管貼在哪個對象,你都必須先有對象才可以,不然變量貼誰去?
另外,盡管 Python 在創(chuàng)建變量的時候不需要指定類型,但 Python 是強類型語言,強類型語言,強類型語言,重要的事情說三遍。?而且是動態(tài)強類型,因為類型的強弱和是否需要顯示聲明類型之間沒有關(guān)系。
可變對象與不可變對象
我們說一個對象其實就是一片被分配的內(nèi)存空間,內(nèi)存中存儲了相應(yīng)的值,不過這些空間可以是連續(xù)的,也可以是不連續(xù)的。
不可變對象一旦創(chuàng)建,其內(nèi)存中存儲的值就不可以再修改了。如果想修改,只能創(chuàng)建一個新的對象,然后讓變量指向新的對象,所以前后的地址會發(fā)生改變。而可變對象在創(chuàng)建之后,其存儲的值可以動態(tài)修改。
像整型就是一個不可變對象。
>>>?a?=?666
>>>?id(a)
1365442984464
>>>?a?+=?1
>>>?id(a)
1365444032848
>>>
我們看到在對 a 執(zhí)行+1操作時,前后地址發(fā)生了變化,所以整型不支持本地修改,因此是一個不可變對象;

原來a = 666,而我們說操作一個變量等于操作這個變量指向的內(nèi)存,所以a+=1,會將a指向的整型對象666和1進行加法運算,得到667。所以會開辟新的空間來存儲這個667,然后讓a指向這片新的空間,至于原來的666所占的空間怎么辦,Python 解釋器會看它的引用計數(shù),如果不為0代表還有變量引用(指向)它,如果為0證明沒有變量引用了,所以會被回收。
關(guān)于引用計數(shù),我們后面會詳細說,目前只需要知道當(dāng)一個對象被一個變量引用的時候,那么該對象的引用計數(shù)就會加1。有幾個變量引用,那么它的引用計數(shù)就是幾。
可能有人覺得,每次都要創(chuàng)建新對象,銷毀舊對象,效率肯定會很低吧。事實上確實如此,但是后面我們會從源碼的角度上來看 Python 如何通過小整數(shù)對象池等手段進行優(yōu)化。
而列表是一個可變對象,它是可以修改的。
這里先多提一句,Python中的對象本質(zhì)上就是C中malloc函數(shù)為結(jié)構(gòu)體實例在堆區(qū)申請的一塊內(nèi)存。Python中的任何對象在C中都會對應(yīng)一個結(jié)構(gòu)體,這個結(jié)構(gòu)體除了存放具體的值之外,還存放了一些額外的信息,這個我們在剖析Python中的內(nèi)置類型的實例對象的時候會細說。
首先Python中列表,當(dāng)然不光是列表,還有元組、集合,這些容器它們的內(nèi)部存儲的也不是具體的對象,而是對象的指針。比如:lst = [1, 2, 3],你以為lst存儲的是三個整型對象嗎?其實不是的,lst存儲的是三個整型對象的指針,當(dāng)我們使用lst[0]的時候,拿到的是第一個元素的指針,但是操作(比如print)的時候會自動操作(print)指針指向的內(nèi)存。
不知道你是否思考過,Python底層是C來實現(xiàn)的,所以Python中的列表的實現(xiàn)必然要借助C中的數(shù)組??晌覀冎繡中的數(shù)組里面的所有元素的類型必須一致,但列表卻可以存放任意的元素,因此從這個角度來講,列表里面的元素它就就不可能是對象,因為不同的對象在底層對應(yīng)的結(jié)構(gòu)體是不同的,所以這個元素只能是指針。
可能有人又好奇了,不同對象的指針也是不同的啊,是的,但C中的指針是可以轉(zhuǎn)化的。Python底層將所有對象的指針,都轉(zhuǎn)成了 PyObject 的指針,這樣不就是同一種類型的指針了嗎?關(guān)于這個PyObject,它是我們后面要剖析的重中之重,這個PyObject貫穿了我們的整個系列。目前只需要知道Python中的列表存儲的值,在底層是通過一個 PyObject * 類型的數(shù)據(jù)來維護的。
>>>?lst?=?[1,?2,?3]
>>>?id(lst)
1365442893952
>>>?lst.append(4)
>>>?lst
[1,?2,?3,?4]
>>>?id(lst)
1365442893952
>>>
我們看到列表在添加元素的時候,前后地址并沒有改變。列表在C中是通過PyListObject實現(xiàn)的,我們在介紹列表的時候會細說。這個PyListObject內(nèi)部除了一些基本信息之外,還有一個成員叫ob_item,它是一個PyObject的二級指針,指向了我們剛才說的 PyObject * 類型的數(shù)組的首個元素的地址。
結(jié)構(gòu)圖如下:

顯然圖中的指針數(shù)組是用來存儲具體的對象的指針的,每一個指針都指向了相應(yīng)的對象(這里是整型對象)??赡苡腥俗⒁獾剑蛯ο蟮捻樞蛴悬c怪,其實我是故意這么畫的。因為 PyObject * 數(shù)組內(nèi)部的元素是連續(xù)且有順序的,但是指向的整型對象則是存儲在堆區(qū)的,它們的位置是任意性的。但是不管這些整型對象存儲在堆區(qū)的什么位置,它們和數(shù)組中的指針都是一一對應(yīng)的,我們通過索引是可以正確獲取到指向的對象的。
另外我們還可以看到一個現(xiàn)象,那就是Python中的列表在底層是分開存儲的,因為PyListObject結(jié)構(gòu)體實例并沒有存儲相應(yīng)的指針數(shù)組,而是存儲了指向這個指針數(shù)組的二級指針。顯然我們添加、刪除、修改元素等操作,都是通過這個二級指針來間接操作這個指針數(shù)組。
為什么要這么做?
因為在 Python 中一個對象一旦被創(chuàng)建,那么它在內(nèi)存中的大小就不可以變了。所以這就意味著那些可以容納可變長度數(shù)據(jù)的可變對象,要在內(nèi)部維護一個指向可變大小的內(nèi)存區(qū)域的指針。而我們看到 PyListObject 正是這么做的,指針數(shù)組的長度、內(nèi)存大小是可變的,所以 PyListObject 內(nèi)部并沒有直接存儲它,而是存儲了指向它的二級指針。但是 Python 在計算內(nèi)存大小的時候是會將這個指針數(shù)組也算進去的,所以 Python 中列表的大小是可變的,但是底層對應(yīng)的 PyListObject 實例的大小是不變的,因為可變長度的指針數(shù)組沒有存在 PyListObject 里面。但為什么要這么設(shè)計呢?
這么做的原因就在于,遵循這樣的規(guī)則可以使通過指針維護對象的工作變得非常簡單。一旦允許對象的大小可在運行期改變,那么我們就可以考慮如下場景。在內(nèi)存中有對象A,并且其后面緊跟著對象B。如果運行的某個時候,A的大小增大了,這就意味著必須將A整個移動到內(nèi)存中的其他位置,否則A增大的部分會覆蓋掉原本屬于B的數(shù)據(jù)。只要將A移動到內(nèi)存的其他位置,那么所有指向A的指針就必須立即得到更新。可想而知這樣的工作是多么的繁瑣,而通過一個指針去操作就變得簡單多了。
定長對象與變長對象
Python 中一個對象占用的內(nèi)存有多大呢?相同類型的實例對象的大小是否相同呢?試一下就知道了,我們可以通過 sys 模塊中 getsizeof 函數(shù)查看一個對象所占的內(nèi)存。
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
我們看到整型對象的大小不同,所占的內(nèi)存也不同,像這種內(nèi)存大小不固定的對象,我們稱之為變長對象;而浮點數(shù)所占的內(nèi)存都是一樣的,像這種內(nèi)存大小固定的對象,我們稱之為定長對象。
至于 Python 是如何計算對象所占的內(nèi)存,我們在剖析具體對象的時候會說,因為這要涉及到底層對應(yīng)的結(jié)構(gòu)體。
而且我們知道 Python 中的整數(shù)是不會溢出的,而C中的整型顯然是有最大范圍的,那么Python是如何做到的呢?答案是Python在底層是通過C的32位整型數(shù)組來存儲自身的整型對象的,通過多個32位整型組合起來,以支持存儲更大的數(shù)值,所以整型越大,就需要越多的32位整數(shù)。而32位整數(shù)是4字節(jié),所以我們上面代碼中的那些整型,都是4字節(jié)、4字節(jié)的增長。
當(dāng)然Python中的對象在底層都是一個結(jié)構(gòu)體,這個結(jié)構(gòu)體中除了維護具體的值之外,還有其它的成員信息,在計算內(nèi)存大小的時候,它們也是要考慮在內(nèi)的,當(dāng)然這些我們后面會說。
而浮點數(shù)的大小是不變的,因為Python的浮點數(shù)的值在C中是通過一個double來維護的。而C中的值的類型一旦確定,大小就不變了,所以Python的float也是不變的。
但是既然是固定的類型,肯定范圍是有限的,所以當(dāng)浮點數(shù)不斷增大,會犧牲精度來進行存儲。如果實在過大,那么會拋出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
我們看到多了兩個字符,多了兩個字節(jié),這很好理解。但是這些說明了一個空字符串要占49個字節(jié),我們來看一下。
import?sys
print(sys.getsizeof(""))??#?49
顯然是的,顯然這 49 個字節(jié)是用來維護其它成員信息的,因為底層的結(jié)構(gòu)體除了維護具體的值之外,還要維護其它的信息,比如:引用計數(shù)等等,這些在分析源碼的時候會詳細說。
小結(jié)
我們這一節(jié)介紹了 Python 中的對象體系,我們說 Python 中一切皆對象,類型對象和實例對象都屬于對象;還說了對象的種類,根據(jù)是否支持本地修改可以分為可變對象和不可變對象,根據(jù)占用的內(nèi)存是否不變可以分為定長對象和變長對象;還說了 Python 中變量的本質(zhì),Python 中的變量本質(zhì)上是一個指針,而變量的名字則存儲在對應(yīng)的名字空間(或者說命名空間)中,當(dāng)然名字空間我們沒有說,是因為這些在后續(xù)系列會詳細說(又是后續(xù), 不管咋樣, 坑先挖出來),不過這里可以先補充一下。
名字空間分為:全局名字空間(存儲全局變量)、局部名字空間(存儲局部變量)、閉包名字空間(存儲閉包變量)、內(nèi)建名字空間(存儲內(nèi)置變量, 比如 int、str, 它們都在這里),而名字空間又分為靜態(tài)名字空間和動態(tài)名字空間:比如局部名字空間,因為函數(shù)中的局部變量在編譯的時候就可以確定,所以函數(shù)對應(yīng)的局部名字空間使用一個數(shù)組存儲;而全局變量在運行時可以進行動態(tài)添加、刪除,因此全局名字空間使用的是一個字典來保存,字典的 key 就是變量的名字(依舊是個指針,底層是指向字符串(PyUnicodeObject)的指針),字典的 value 就是變量指向的對象的指針(或者說變量本身)。
a?=?123
b?=?"xxx"
#?通過globals()即可獲取全局名字空間
print(globals())??#{...,?'a':?123,?'b':?'xxx'}
#?我們看到雖然顯示的是變量名和變量指向的值
#?但是在底層,字典存儲的鍵值對也是指向具體對象的指針
#?只不過我們說操作指針會操作指向的內(nèi)存,所以這里print打印之后,顯示的也是具體的值,但是存儲的是指針
#?至于對象本身,則存儲在堆區(qū),并且被指針指向
#??此外,我們往全局名字空間中設(shè)置一個鍵值對,也等價于創(chuàng)建了一個全局變量
globals()["c"]?=?"hello"
print(c)??#?hello
#?此外這個全局名字空間是唯一的,即使你把它放在函數(shù)中也是一樣
def?foo():
????globals()["d"]?=?"古明地覺"
#?foo一旦執(zhí)行,{"d":?"古明地覺"}就設(shè)置進了全局名字空間中
foo()??
print(d)??#?古明地覺
作者:古明地盆
https://www.cnblogs.com/traditional/p/13391098.html
- EOF -
推薦閱讀

Python 搶火車票神器,支持候補搶票

