type 與 object 之間的恩怨糾葛

估計很多人都會有這樣一個困惑,object 的類型是 type,但它同時又是 type 的基類,這是怎麼做到的?帶着這個疑問,我們開始本文的內容。

在學習 Python 的時候,你肯定聽過這麼一句話:Python 中一切皆對象。沒錯,在 Python 世界裏,一切都是對象。整數是一個對象、字符串是一個對象、字典是一個對象,甚至 int, str, list 等等,再加上我們使用 class 關鍵字自定義的類,它們也是對象。

像 int, str, list 等基本類型,以及我們自定義的類,由於它們可以表示類型,因此我們稱之爲類型對象;類型對象實例化得到的對象,我們稱之爲實例對象。但不管是哪種對象,它們都屬於對象。

因此 Python 將面向對象理念貫徹的非常徹底,面向對象中的類和對象在 Python 中都是通過對象實現的。

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

我們舉個例子:

# dict 是一個類,因此它屬於類型對象
# 類型對象實例化得到的對象屬於實例對象
print(dict)
"""
<class 'dict'>
"""
print(dict(a=1, b=2))
"""
{'a': 1, 'b': 2}
"""

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

而如果想查看一個對象的類型,可以使用 type,或者通過對象的 class 屬性。

numbers = [1, 2, 3]
# 查看類型
print(type(numbers))
"""
<class 'list'>
"""
print(numbers.__class__)
"""
<class 'list'>
"""

如果想判斷一個對象是不是指定類型的實例對象,可以使用 isinstance。

numbers = [1, 2, 3]
# 判斷是不是指定類型的實例對象
print(isinstance(numbers, list))
"""
True
"""

但是問題來了,按照面向對象的理論來說,對象是由類實例化得到的,這在 Python 中也是適用的。既然是對象,那麼就必定有一個類來實例化它,換句話說對象一定要有類型。

至於一個對象的類型是什麼,就看這個對象是被誰實例化的,被誰實例化那麼類型就是誰,比如列表的類型是 list,字典的類型是 dict 等等。

而我們說 Python 中一切皆對象,所以像 int, str, tuple 這些內置的類對象也是具有相應的類型的,那麼它們的類型又是誰呢?

我們使用 type 查看一下。

>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(dict)
<class 'type'>
>>> type(type)
<class 'type'>

我們看到類型對象的類型,無一例外都是 type。而 type 我們也稱其爲元類,表示類型對象的類型。至於 type 本身,它的類型還是 type,所以它連自己都沒放過,把自己都變成自己的對象了。

因此在 Python 中,你能看到的任何對象都是有類型的,我們可以使用 type 查看,也可以獲取該對象的 class 屬性查看。所以:實例對象、類型對象、元類,Python 中任何一個對象都逃不過這三種身份。

到這裏可能有人會發現一個有意思的點,我們說 int 是一個類對象,這顯然是沒有問題的。因爲站在整數(比如 123)的角度上,int 是一個不折不扣的類對象;但如果站在 type 的角度上呢?顯然我們又可以將 int 理解爲實例對象,因此 class 具有二象性。

至於 type 也是同理,雖然它是元類,但本質上也是一個類對象。

注:不僅 type 是元類,那些繼承了 type 的類也可以叫做元類。

這些概念上的東西讀起來可能會有一點繞,但如果實際動手敲一敲代碼的話,還是很好理解的。

然後 Python 中還有一個關鍵的類型(對象),叫做 object,它是所有類型對象的基類。不管是什麼類,內置的類也好,我們自定義的類也罷,它們都繼承自 object。因此 object 是所有類型對象的基類、或者說父類。

那如果我們想獲取一個類都繼承了哪些基類,該怎麼做呢?方式有三種:

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__)  
"""
<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'>)
"""

而如果想查看某個類型是不是另一個類型的子類,可以通過 issubclass。

print(issubclass(str, object))
"""
True
"""

因此,到目前爲止,關於 type 和 object,我們可以得出以下兩個結論:

但要注意的是,我們說 type 的類型還是 type,但 object 的基類則不再是 object,而是 None。

print(
    type.__class__
)  # <class 'type'>

# 注:以下打印結果容易讓人產生誤解
# 它表達的含義是 object 的基類爲空
# 而不是說 object 繼承 None
print(
    object.__base__
)  # None

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

我們用一張圖將對象之間的關係總結一下:

因此 Python 算是將一切皆對象的理念貫徹到了極致,也正因爲如此,Python 才具有如此優秀的動態特性。

但是還沒結束,我們再重新審視一下上面那張圖,會發現裏面有兩個箭頭看起來非常的奇怪。object 的類型是 type,type 又繼承了 object。

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

因爲 type 是所有類的元類,而 object 是所有類的基類,這就說明 type 要繼承自 object,而 object 的類型是 type。很多人都會對這一點感到奇怪,這難道不是一個先有雞還是先有蛋的問題嗎?其實不是的,這兩個對象是共存的,它們之間的定義其實是互相依賴的。而具體是怎麼一回事,我們一點一點分析。

首先在這裏必須要澄清一個事實,類對象的類型是 type,這句話是沒有問題的;但如果說類對象都是由 type 創建的,就有些爭議了。因爲 type 能夠創建的是自定義的類,而內置的類在底層是預先定義好的。

# int、tuple、dict 等內置類型
# 在底層是預先定義好的,以全局變量的形式存在
# 我們直接就可以拿來用
print(int)  # <class 'int'>
print(tuple)  # <class 'tuple'>

# 但對於自定義的類,顯然就需要在運行時動態創建了
# 而創建這一過程,就交給 type 來做
class Girl:
    pass

而 type 也只能對自定義類進行屬性上的增刪改,內置的類則不行。

class Girl:
    pass

# 給類對象增加一個成員函數
type.__setattr__(
    Girl,
    "info",
    lambda self: "name: 古明地覺, age: 17"
)
# 實例化之後就可以調用了
print(Girl().info())  # name: 古明地覺, age: 17

# 但內置的類對象,type 是無法修改的
try:
    type.__setattr__(int, "a""b")
except TypeError as e:
    print(e)
"""
can't set attributes of built-in/extension type 'int'
"""

而 Python 所有內置的類對象,在解釋器看來,都是同級別的。因爲它們都是由同一個結構體實例化得到的。

所有內置的類對象都是 PyTypeObject 結構體實例,只不過結構體字段的值不同,得到的類也不同。所以元類 type 和普通的類對象,在解釋器看來都是等價的。

在解釋器看來,它們無一例外都是 PyTypeObject 結構體實例。換句話說,它們都是基於這個結構體創建出的全局變量罷了,這些變量代表的就是 Python 的類。

而每一個對象都有引用計數和類型,然後解釋器將這些類對象的類型都設置成了 type,我們以 object 爲例:

我們看到它的類型被設置成了 type,所以結論很清晰了,雖然內置類對象可以看做是 type 的實例對象,但它卻不是由 type 實例化得到的。所有內置的類對象,在底層都是預定義好的,以靜態全局變量的形式出現。

至於 type 也是同理:

解釋器只是將 type 的類型設置成了它自身而已,所以內置的類對象之間不存在誰創建誰。它們都是預定義好的,只是在定義的時候,將自身的類型設置成 type 而已,包括 type 本身。這樣一來,每一個對象都會具有一個類型,從而將面向對象理念貫徹的更加徹底。

print(int.__class__)
print(tuple.__class__)
print(set.__class__)
print(type.__class__)
"""
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
"""

print(
    type.__class__.__class__.__class__ is type
)  # True

print(
    type(type(type(type(type(type))))) is type
)  # True

現在 object 的類型是 type 我們已經搞清楚是怎麼一回事了,然後是基類的問題。PyTypeObject 結構體內部有一個 tp_base,它表示的就是類對象繼承的基類。

但令我們喫鯨的是,它的 tp_base 居然是個 0,如果爲 0 的話則表示沒有這個屬性。不是說 type 的基類是 object 嗎?爲啥 tp_base 是 0 呢。

事實上如果你去看 PyFloat_Type 以及其它類型的話,會發現它們內部的 tp_base 也是 0。爲 0 的原因就在於我們目前看到的類型對象是一個半成品,因爲 Python 的動態性,顯然不可能在定義的時候就將所有成員屬性都設置好、然後解釋器一啓動就得到我們平時使用的類型對象。

目前看到的類型對象是一個半成品,有一部分成員屬性是在解釋器啓動之後再動態完善的,而這個完善的過程被稱爲類型對象的初始化,它由函數 PyType_Ready 負責。

首先代碼中的 type 只是一個普通的參數,當解釋器發現一個類對象還沒有初始化時,會將其作爲參數傳遞進來,進行初始化。base 則顯然是它的基類,然後如果基類爲空,並且該類不是 object 的話,那麼就將它的基類設置成 object。所以 Python3 中,所有的類默認都繼承 object,當然除了 object 本身。

因此到目前爲止,type 和 object 之間的恩怨糾葛算是真相大白了,總結一下:

1)和自定義類不同,內置的類不是由 type 實例化得到的,它們都是在底層預先定義好的,不存在誰創建誰。只是內置的類在定義的時候,它們的類型也都被設置成了 type。這樣不管是內置的類,還是自定義類,在調用時都會執行 type 的 call 方法,從而讓它們的行爲是一致的。

2)雖然內置的類在底層預定義好了,但還有一些瑕疵,因爲有一部分邏輯無法以源碼的形式體現,只能在解釋器啓動的時候再動態完善。而這個完善的過程,便包含了基類的填充,會將基類設置成 object。

所以 type 和 object 是同時出現的,它們的存在需要依賴彼此。首先這兩者會以不完全體的形式定義在源碼中,並且在定義的時候將 object 的類型設置成 type;然後當解釋器啓動的時候,再經過動態完善,進化成完全體,而進化的過程中會將 type 的基類設置成 object。

所以 object 的類型是 type,type 繼承 object 就是這麼來的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/H-7LzpgWWDnZUuQxpWeWjw