深度解密 Python 的浮點數是怎么實現的?
楔子
從現在開始,我們就來分析 Python 的內置對象,看看它們在底層是如何實現的。但說實話,我們在前面幾篇文章中介紹對象的時候,已經說了不少了,不過從現在開始要進行更深入的分析。
除了對象本身,還要看對象支持的操作在底層是如何實現的。我們首先以浮點數為例,因為它是最簡單的,沒錯,浮點數比整數要簡單,至于為什么,等我們分析整數的時候就知道了。
浮點數的底層結構
要想搞懂浮點數的實現原理,就要知道它在底層是怎么定義的,當然在這之前我們已經見過它很多遍了。
// Include/cpython/floatobject.h
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
它包含了一個公共頭部 PyObject 和一個 double 類型的 ob_fval 字段,毫無疑問這個 ob_fval 字段負責存儲浮點數的具體數值。
我們以 e = 2.71 為例,底層結構如下。
圖片
還是很簡單的,每個對象在底層都是由結構體表示的,這些結構體中有的字段負責維護對象的元信息,有的字段負責維護具體的值。比如這里的 2.71,總要有一個字段來存儲 2.71 這個值,而這個字段就是 ob_fval。所以浮點數的結構非常簡單,直接使用一個 C 的 double 來維護。
假設我們要將兩個浮點數相加,相信你已經知道解釋器會如何做了?通過 PyFloat_AsDouble 將兩個浮點數的 ob_fval 抽出來,然后相加,最后再根據相加的結果創建一個新的 PyFloatObject 即可。
浮點數是怎么創建的
下面來看看浮點數是如何創建的,在前面的文章中,我們說對象可以使用對應的特定類型 API 創建,也可以通過調用類型對象創建。
調用類型對象 float 創建實例對象,解釋器會執行元類 type 的 tp_call,它指向了 type_call 函數。然后 type_call 內部會先調用類型對象(這里是 float)的 tp_new 為其實例對象申請一份空間,申請完畢之后對象就已經創建好了。然后再調用 tp_init,并將實例對象作為參數傳遞進去,進行初始化,也就是設置屬性。
但是對于 float 來說,它內部的 tp_init 字段為 0,也就是空。
圖片
這就說明 float 沒有 __init__,因為浮點數太過簡單,只需要一個 tp_new 即可。我們舉個例子:
class Girl1:
def __init__(self, name, age):
self.name = name
self.age = age
# __new__ 負責開辟空間、生成實例對象
# __init__ 負責給實例對象綁定屬性
# 但其實 __init__ 所做的工作可以直接在 __new__ 當中完成
# 換言之有 __new__ 就足夠了,其實可以沒有 __init__
# 我們將上面的例子改寫一下
class Girl2:
def __new__(cls, name, age):
instance = object.__new__(cls)
instance.name = name
instance.age = age
return instance
g1 = Girl1("古明地覺", 16)
g2 = Girl2("古明地覺", 16)
print(g1.__dict__ == g2.__dict__) # True
我們看到效果是等價的,因為 __init__ 負責給 self 綁定屬性,而這個 self 是 __new__ 返回的。那么很明顯,我們也可以在 __new__ 當中綁定屬性,而不需要 __init__。