解密 Python 的變量和對(duì)象,它們之間有什么區(qū)別和聯(lián)系呢?
Python 中一切皆對(duì)象
在學(xué)習(xí) Python 的時(shí)候,你肯定聽(tīng)過(guò)這么一句話:Python 中一切皆對(duì)象。沒(méi)錯(cuò),在 Python 世界里,一切都是對(duì)象。整數(shù)是一個(gè)對(duì)象、字符串是一個(gè)對(duì)象、字典是一個(gè)對(duì)象,甚至 int, str, list 以及我們使用 class 關(guān)鍵字自定義的類(lèi),它們也是對(duì)象。
像 int, str, list 等基本類(lèi)型,以及自定義的類(lèi),由于它們可以表示類(lèi)型,因此我們稱之為類(lèi)型對(duì)象;類(lèi)型對(duì)象實(shí)例化得到的對(duì)象,我們稱之為實(shí)例對(duì)象。但不管是哪種對(duì)象,它們都屬于對(duì)象。
因此 Python 將面向?qū)ο罄砟钬瀼氐姆浅氐?,面向?qū)ο笾械念?lèi)和對(duì)象在 Python 中都是通過(guò)對(duì)象實(shí)現(xiàn)的。
在面向?qū)ο罄碚撝?,存在著?lèi)和對(duì)象兩個(gè)概念,像 int、dict、tuple、以及使用 class 關(guān)鍵字自定義的類(lèi)型對(duì)象實(shí)現(xiàn)了面向?qū)ο罄碚撝蓄?lèi)的概念,而 123、3.14,"string" 等等這些實(shí)例對(duì)象則實(shí)現(xiàn)了面向?qū)ο罄碚撝袑?duì)象的概念。但在 Python 里面,面向?qū)ο蟮念?lèi)和對(duì)象都是通過(guò)對(duì)象實(shí)現(xiàn)的。
我們舉個(gè)例子:
# dict 是一個(gè)類(lèi),因此它屬于類(lèi)型對(duì)象
# 類(lèi)型對(duì)象實(shí)例化得到的對(duì)象屬于實(shí)例對(duì)象
print(dict)
"""
<class 'dict'>
"""
print(dict(a=1, b=2))
"""
{'a': 1, 'b': 2}
"""
因此可以用一張圖來(lái)描述面向?qū)ο笤?Python 中的體現(xiàn)。
圖片
而如果想查看一個(gè)對(duì)象的類(lèi)型,可以使用 type,或者通過(guò)對(duì)象的 __class__ 屬性。
numbers = [1, 2, 3]
# 查看類(lèi)型
print(type(numbers))
"""
<class 'list'>
"""
print(numbers.__class__)
"""
<class 'list'>
"""
如果想判斷一個(gè)對(duì)象是不是指定類(lèi)型的實(shí)例對(duì)象,可以使用 isinstance。
numbers = [1, 2, 3]
# 判斷是不是指定類(lèi)型的實(shí)例對(duì)象
print(isinstance(numbers, list))
"""
True
"""
但是問(wèn)題來(lái)了,按照面向?qū)ο蟮睦碚搧?lái)說(shuō),對(duì)象是由類(lèi)實(shí)例化得到的,這在 Python 中也是適用的。既然是對(duì)象,那么就必定有一個(gè)類(lèi)來(lái)實(shí)例化它,換句話說(shuō)對(duì)象一定要有類(lèi)型。
至于一個(gè)對(duì)象的類(lèi)型是什么,就看這個(gè)對(duì)象是被誰(shuí)實(shí)例化的,被誰(shuí)實(shí)例化那么類(lèi)型就是誰(shuí),比如列表的類(lèi)型是 list,字典的類(lèi)型是 dict 等等。
而 Python 中一切皆對(duì)象,所以像 int, str, tuple 這些內(nèi)置的類(lèi)對(duì)象也是具有相應(yīng)的類(lèi)型的,那么它們的類(lèi)型又是誰(shuí)呢?使用 type 查看一下就知道了。
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(dict)
<class 'type'>
>>> type(type)
<class 'type'>
我們看到類(lèi)型對(duì)象的類(lèi)型,無(wú)一例外都是 type。而 type 我們也稱其為元類(lèi),表示類(lèi)型對(duì)象的類(lèi)型。至于 type 本身,它的類(lèi)型還是 type,所以它連自己都沒(méi)放過(guò),把自己都變成自己的對(duì)象了。
因此在 Python 中,你能看到的任何對(duì)象都是有類(lèi)型的,可以使用 type 查看,也可以獲取該對(duì)象的 __class__ 屬性查看。所以:實(shí)例對(duì)象、類(lèi)型對(duì)象、元類(lèi),Python 中任何一個(gè)對(duì)象都逃不過(guò)這三種身份。
到這里可能有人會(huì)發(fā)現(xiàn)一個(gè)有意思的點(diǎn),我們說(shuō) int 是一個(gè)類(lèi)對(duì)象,這顯然是沒(méi)有問(wèn)題的。因?yàn)檎驹谡麛?shù)(比如 123)的角度上,int 是一個(gè)不折不扣的類(lèi)對(duì)象;但如果站在 type 的角度上呢?顯然我們又可以將 int 理解為實(shí)例對(duì)象,因此 class 具有二象性。
至于 type 也是同理,雖然它是元類(lèi),但本質(zhì)上也是一個(gè)類(lèi)對(duì)象。
注:不僅 type 是元類(lèi),那些繼承了 type 的類(lèi)也可以叫做元類(lèi)。
然后 Python 中還有一個(gè)關(guān)鍵的類(lèi)型(對(duì)象),叫做 object,它是所有類(lèi)型對(duì)象的基類(lèi)。不管是什么類(lèi),內(nèi)置的類(lèi)也好,我們自定義的類(lèi)也罷,它們都繼承自 object。因此 object 是所有類(lèi)型對(duì)象的基類(lèi)、或者說(shuō)父類(lèi)。
那如果我們想獲取一個(gè)類(lèi)都繼承了哪些基類(lèi),該怎么做呢?方式有三種:
class A: pass
class B: pass
class C(A): pass
class D(B, C): pass
# 首先 D 繼承自 B 和 C, C 又繼承 A
# 我們現(xiàn)在要來(lái)查看 D 繼承的父類(lèi)
# 方法一: 使用 __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__:如果繼承了多個(gè)類(lèi),那么只顯示繼承的第一個(gè)類(lèi),沒(méi)有顯式繼承則返回 <class 'object'>
- __bases__:返回一個(gè)元組,會(huì)顯示所有直接繼承的父類(lèi),沒(méi)有顯式繼承則返回 (<class 'object'>,)
- __mro__: mro(Method Resolution Order)表示方法查找順序,會(huì)從自身出發(fā),找到最頂層的父類(lèi)。因此返回自身、繼承的基類(lèi)、以及基類(lèi)繼承的基類(lèi), 一直找到 object
而如果想查看某個(gè)類(lèi)型是不是另一個(gè)類(lèi)型的子類(lèi),可以通過(guò) issubclass。
print(issubclass(str, object))
"""
True
"""
因此,我們可以得出以下兩個(gè)結(jié)論:
- type 站在類(lèi)型金字塔的最頂端,任何一個(gè)對(duì)象按照類(lèi)型追根溯源,最終得到的都是 type;
- object 站在繼承金字塔的最頂端,任何一個(gè)類(lèi)型對(duì)象按照繼承關(guān)系追根溯源,最終得到的都是 object;
但要注意的是,我們說(shuō) type 的類(lèi)型還是 type,但 object 的基類(lèi)則不再是 object,而是 None。
print(
type.__class__
) # <class 'type'>
# 注:以下打印結(jié)果容易讓人產(chǎn)生誤解
# 它表達(dá)的含義是 object 的基類(lèi)為空
# 而不是說(shuō) object 繼承 None
print(
object.__base__
) # None
但為什么 object 的基類(lèi)是 None,而不是它自身呢?其實(shí)答案很簡(jiǎn)單,Python 在查找屬性或方法的時(shí)候,自身如果沒(méi)有的話,會(huì)按照 __mro__ 指定的順序去基類(lèi)中查找。所以繼承鏈一定會(huì)有一個(gè)終點(diǎn),否則就會(huì)像沒(méi)有出口的遞歸一樣出現(xiàn)死循環(huán)了。
我們用一張圖將對(duì)象之間的關(guān)系總結(jié)一下:
圖片
- 實(shí)例對(duì)象的類(lèi)型是類(lèi)型對(duì)象,類(lèi)型對(duì)象的類(lèi)型是元類(lèi);
- 所有類(lèi)型對(duì)象的基類(lèi)都收斂于 object;
- 所有對(duì)象的類(lèi)型都收斂于 type;
因此 Python 算是將一切皆對(duì)象的理念貫徹到了極致,也正因?yàn)槿绱?,Python 才具有如此優(yōu)秀的動(dòng)態(tài)特性。
但是還沒(méi)結(jié)束,我們?cè)僦匦聦徱曇幌律厦婺菑垐D,會(huì)發(fā)現(xiàn)里面有兩個(gè)箭頭看起來(lái)非常的奇怪。object 的類(lèi)型是 type,type 又繼承了 object。
>>> type.__base__
<class 'object'>
>>> object.__class__
<class 'type'>
因?yàn)?nbsp;type 是所有類(lèi)的元類(lèi),而 object 是所有類(lèi)的基類(lèi),這就說(shuō)明 type 要繼承自 object,而 object 的類(lèi)型是 type。很多人都會(huì)對(duì)這一點(diǎn)感到奇怪,這難道不是一個(gè)先有雞還是先有蛋的問(wèn)題嗎?其實(shí)不是的,這兩個(gè)對(duì)象是共存的,它們之間的定義其實(shí)是互相依賴的。而具體是怎么一回事,我們后續(xù)分析。
Python 的變量其實(shí)是指針
Python 的變量只是一個(gè)名字,如果站在 C 語(yǔ)言的角度來(lái)看,那么就是一個(gè)指針。所以 Python 的變量保存的其實(shí)是對(duì)象的內(nèi)存地址,或者說(shuō)指針,而指針指向的內(nèi)存存儲(chǔ)的才是對(duì)象。
所以在 Python 中,我們都說(shuō)變量指向了某個(gè)對(duì)象。在其它靜態(tài)語(yǔ)言中,變量相當(dāng)于是為某塊內(nèi)存起的別名,獲取變量等于獲取這塊內(nèi)存所存儲(chǔ)的值。而 Python 中變量代表的內(nèi)存所存儲(chǔ)的不是對(duì)象,而是對(duì)象的指針(或者說(shuō)引用)。
我們舉例說(shuō)明,看一段 C 代碼。
#include <stdio.h>
void main()
{
int a = 666;
printf("address of a = %p\n", &a);
a = 667;
printf("address of a = %p\n", &a);
}
編譯執(zhí)行一下:
圖片
賦值前后地址都是 0x7fff9eda521c,沒(méi)有變化,再來(lái)看一段 Python 代碼。
a = 666
print(hex(id(a))) # 0x7febf803a3d0
a = 667
print(hex(id(a))) # 0x7fec180677b0
我們看到 Python 里面輸出的地址發(fā)生了變化,下面分析一下原因。
首先在 C 中,創(chuàng)建一個(gè)變量的時(shí)候必須規(guī)定好類(lèi)型,比如 int a = 666,那么變量 a 就是 int 類(lèi)型,以后在所處的作用域中就不可以變了。如果這時(shí)候再設(shè)置 a = 777,那么等于是把內(nèi)存中存儲(chǔ)的 666 換成 777,a 的地址和類(lèi)型是不會(huì)變化的。
而在 Python 中,a = 666 等于是先開(kāi)辟一塊內(nèi)存,存儲(chǔ)的值為 666,然后讓變量 a 指向這片內(nèi)存,或者說(shuō)讓變量 a 保存這塊內(nèi)存的地址。然后 a = 777 的時(shí)候,再開(kāi)辟一塊內(nèi)存,然后讓 a 指向存儲(chǔ) 777 的內(nèi)存,由于是兩塊不同的內(nèi)存,所以它們的地址是不一樣的。
圖片
所以 Python 的變量只是一個(gè)和對(duì)象關(guān)聯(lián)的名字,它代表的是對(duì)象的指針。換句話說(shuō) Python 的變量就是個(gè)便利貼,可以貼在任何對(duì)象上,一旦貼上去了,就代表這個(gè)對(duì)象被引用了。
值傳遞?引用傳遞?
再來(lái)看看變量之間的傳遞,在 Python 中是如何體現(xiàn)的。
a = 666
print(hex(id(a))) # 0x1f4e8ca7fb0
b = a
print(hex(id(b))) # 0x1f4e8ca7fb0
我們看到打印的地址是一樣的,再用一張圖解釋一下。
圖片
a = 666 的時(shí)候,先開(kāi)辟一份內(nèi)存,再讓 a 存儲(chǔ)對(duì)應(yīng)內(nèi)存的地址;然后 b = a 的時(shí)候,會(huì)把 a 拷貝一份給 b,所以 b 和 a 存儲(chǔ)了相同的地址,它們都指向了同一個(gè)對(duì)象。
因此說(shuō) Python 是值傳遞、或者引用傳遞都是不準(zhǔn)確的,準(zhǔn)確的說(shuō) Python 是變量的值傳遞,對(duì)象的引用傳遞。因?yàn)?Python 的變量可以認(rèn)為是 C 的一個(gè)指針,在 b = a 的時(shí)候,等于把 a 指向的對(duì)象的地址(a 本身)拷貝一份給 b,所以對(duì)于變量來(lái)說(shuō)是值傳遞;然后 a 和 b 又都是指向?qū)ο蟮闹羔?,因此?duì)于對(duì)象來(lái)說(shuō)是引用傳遞。
在這個(gè)過(guò)程中,對(duì)象沒(méi)有重復(fù)創(chuàng)建,它只是多了一個(gè)引用。
另外還有最關(guān)鍵的一點(diǎn),Python 的變量是一個(gè)指針,當(dāng)傳遞變量的時(shí)候,傳遞的是指針;但是在操作變量的時(shí)候,會(huì)操作變量指向的內(nèi)存。所以 id(a) 獲取的不是 a 的地址,而是 a 指向的內(nèi)存的地址(在底層其實(shí)就是 a 本身);同理 b = a,是將 a 本身,或者說(shuō)將 a 存儲(chǔ)的、指向某個(gè)具體的對(duì)象的地址傳遞給了 b。
另外在 C 的層面,顯然 a 和 b 屬于指針變量,那么 a 和 b 有沒(méi)有地址呢?顯然是有的,只不過(guò)在 Python 中是獲取不到的,解釋器只允許獲取對(duì)象的地址。
我們?cè)倥e個(gè)函數(shù)的例子:
def some_func(num):
print("address of local num", hex(id(num)))
num = 667
print("address of local num", hex(id(num)))
num = 666
print("address of global num", hex(id(num)))
some_func(num)
"""
address of global num 0x2356cd698d0
address of local num 0x2356cd698d0
address of local num 0x2356c457f90
"""
函數(shù)的參數(shù)也是一個(gè)變量,所以 some_func(num) 其實(shí)就是把全局變量 num 存儲(chǔ)的對(duì)象的地址拷貝一份給局部變量 num,所以兩個(gè) num 指向了同一個(gè)對(duì)象,打印的地址相同。
然后在函數(shù)內(nèi)部執(zhí)行 num = 667,相當(dāng)于讓局部變量指向新的對(duì)象,或者說(shuō)保存新對(duì)象的地址,因此打印的結(jié)果發(fā)生變化。
變量有類(lèi)型嗎?
當(dāng)提到類(lèi)型時(shí),這個(gè)類(lèi)型指的是變量的類(lèi)型還是對(duì)象的類(lèi)型呢?不用想,肯定是對(duì)象的類(lèi)型。因?yàn)?Python 的變量是個(gè)指針,操作指針會(huì)自動(dòng)操作它指向的內(nèi)存,所以使用 type(a) 查看的其實(shí)是變量 a 指向的對(duì)象的類(lèi)型。
那么問(wèn)題來(lái)了,我們?cè)趧?chuàng)建變量的時(shí)候,并沒(méi)有顯式地指定類(lèi)型啊,那么 Python 是如何判斷一個(gè)變量指向的是什么類(lèi)型的數(shù)據(jù)呢?答案是:解釋器是通過(guò)靠猜的方式,通過(guò)賦的值(或者說(shuō)變量引用的值)來(lái)推斷類(lèi)型。
因此在 Python 中,如果你想創(chuàng)建一個(gè)變量,那么必須在創(chuàng)建變量的時(shí)候同時(shí)賦值,否則解釋器就不知道這個(gè)變量指向的數(shù)據(jù)是什么類(lèi)型。所以 Python 是先創(chuàng)建相應(yīng)的值,這個(gè)值在 C 中對(duì)應(yīng)一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體里面有一個(gè)成員專(zhuān)門(mén)用來(lái)存儲(chǔ)該值對(duì)應(yīng)的類(lèi)型,因此在 Python 中,類(lèi)型是和對(duì)象綁定的,而不是和變量。當(dāng)創(chuàng)建完值之后,再讓這個(gè)變量指向它,所以 Python 中是先有值后有變量。
但顯然在 C 里面不是這樣的,因?yàn)?C 的變量代表的內(nèi)存所存儲(chǔ)的就是具體的值,所以在 C 里面可以直接聲明一個(gè)變量的同時(shí)不賦值。因?yàn)?C 要求聲明變量時(shí)必須指定類(lèi)型,因此變量聲明之后,其類(lèi)型和內(nèi)存大小就已經(jīng)固定了。
而 Python 的變量存的是個(gè)地址,它只是指向了某個(gè)對(duì)象,所以由于其便利貼的特性,可以貼在任意對(duì)象上面。但是不管貼在哪個(gè)對(duì)象,都必須先有對(duì)象才可以,不然變量貼誰(shuí)去。
另外,盡管 Python 在創(chuàng)建變量的時(shí)候不需要指定類(lèi)型,但 Python 是強(qiáng)類(lèi)型語(yǔ)言,而且是動(dòng)態(tài)強(qiáng)類(lèi)型。
小結(jié)
以上我們就聊了聊 Python 的變量和對(duì)象,核心就在于:變量保存的不是對(duì)象本身,而是對(duì)象的內(nèi)存地址,站在 C 的角度上看變量就是一個(gè)指針。
盡管 Python 一切皆對(duì)象,但你拿到的都是對(duì)象的指針,變量是一個(gè)指針,函數(shù)是一個(gè)指針,元組、列表、字典里面存儲(chǔ)的還是指針。我們可以想象一下列表,它底層是基于數(shù)組實(shí)現(xiàn)的,由于 C 數(shù)組要求里面的每個(gè)元素的類(lèi)型和大小都相同,因此從這個(gè)角度上講,列表內(nèi)部存儲(chǔ)的只能是指針。