《深度剖析CPython解釋器》5. 解密Python中的整數在底層是如何實現的,以及為什么Python中大整數的運算不會溢出


楔子

這次我們來分析一下Python中的整數是如何實現的,我們知道Python中的整數是不會溢出的,換句話說,它可以計算無窮大的數。只要你的內存足夠,它就能計算,但是對於C來說顯然是不行的,可Python底層又是C實現的,那么它是怎么做到整數不會溢出的呢?

既然想知道答案,那么看一下Python中的整型在底層是怎么定義的就行了。

int實例對象的底層實現

Python中的整數底層對應的結構體是PyLongObject,它位於longobject.h中。

//longobject.h
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

//longintrepr.h
struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

//合起來可以看成
typedef struct {
    PyObject_VAR_HEAD
    digit ob_digit[1];
} PyLongObject;

//如果把這個PyLongObject更細致的展開一下就是
typedef struct {
    Py_ssize_t ob_refcnt; //引用計數
    struct _typeobject *ob_type; //類型
    Py_ssize_t ob_size; //維護的元素個數
    digit ob_digit[1]; //digit類型的數組,長度為1
} PyLongObject;

別的先不說,就沖里面的ob_size我們就可以思考一番。首先Python中的整數有大小、但應該沒有長度的概念吧,那為什么會有一個ob_size呢?從結構體成員來看,這個ob_size指的應該就是ob_digit數組的長度,而這個ob_digit數組顯然只能是用來維護具體的值了。而數組的長度不同,那么對應的整數占用的內存也不同。所以答案出來了,整數雖然沒有我們生活中的那種長度的概念,但它是個變長對象,因為不同的整數占用的內存可能是不一樣的。

因此這個ob_size它指的是底層數組的長度,因為Python中整數對應的值在底層是使用數組來存儲的。盡管它沒有字符串、列表那種長度的概念,或者說無法對整型使用len方法,但它是個變長對象。

那么下面的重點就在這個ob_digit數組了,我們要從它的身上挖掘信息,看看Python中整數對應的值(比如123),是怎么放在這個數組里面的。不過首先我們要看看這個digit是個什么類型,它同樣定義在longintrepr.h中

//PYLONG_BITS_IN_DIGIT是一個宏,如果你的機器是64位的,那么它會被定義為30,32位機器則會被定義為15
//至於這個宏是做什么的我們先不管
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
// ...
#endif

而我們的機器現在基本上都是64位的,所以PYLONG_BITS_IN_DIGIT會等於30,因為digit等價於uint32_t(unsigned int),所以它是一個無符號32位整型。

所以ob_digit這個數組是一個無符號32位整型數組,長度為1。當然這個數組具體多長則取決於你要存儲的Python整數有多大,因為C中數組的長度不屬於類型信息,你可以看成是長度n,而這個n是多少要取決於你的整數大小。顯然整數越大,這個數組就越長,那么占用空間就越大。

搞清楚了PyLongObject里面的所有成員,那么下面我們就來分析ob_digit是怎么存儲Python中的整數,以及Python中的整數為什么不會溢出。

不過說實話,關於Python的整數不會溢出這個問題,其實相信很多人已經有答案了,因為底層是使用數組存儲的嘛,而數組的長度又沒有限制,所以當然不會溢出啦。

另外,還存在一個問題,那就是digit是一個無符號32位整型,那負數怎么存儲?別着急,我們會舉栗說明,將上面的疑問一一解答。

首先如果你是Python的設計者,要保證整數不會溢出,你會怎么辦?我們把問題簡化一下,假設有一個8位的無符號整數類型,我們知道它能表示的最大數字是255,但這時候如果我想表示256,要怎么辦?

可能有人會想,那用兩個數來存儲不就好了。一個存儲255,一個存儲1,將這兩個數放在數組里面。這個答案的話,雖然有些接近,但其實還有很大偏差:那就是我們並不能簡單地按照大小拆分的,256拆分為255和1,要是265就拆分成255和10,而是要通過二進制的方式,我們來簡單看一下。

我們知道8位整數最大就是 2 ^ 8 - 1,也就是它的八位全部都是1,結果是255
所以255對應的數組就是: [255], 因為此時一個8位整數就能存下

但如果是256,那么8位顯然存不下了,此時就還需要一個位
所以這個時候會使用兩個8位整數, 但並不是簡單的相加, 而是使用一個新的8位整數來模擬更高的位

而Python底層也是類似這種做法,但是考慮的會更加全面。下面就以Python中的整數為例,看看底層數組的存儲方式。

整數0:

注意:當要表示的整數為0時,ob_digit這個數組為空,不存儲任何值,ob_size為0,表示這個整數的值為0,這是一種特殊情況。

整數1:

當然存儲的值為1時,ob_size的值就是1,此時ob_digit數組就是[1]。

整數-1:

我們看到ob_digit數組沒有變化,但是ob_size變成了-1,沒錯,整數的正負號是通過這里的ob_size決定的。ob_digit存儲的其實是絕對值,無論n取多少,-nn對應的ob_digit是完全一致的,但是ob_size則互為相反數。所以ob_size除了表示數組的長度之外,還可以表示對應整數的正負。

所以我們之前說整數越大,底層的數組就越長。更准確的說是絕對值越大,底層數組就越長。所以Python在比較兩個整型的大小時,會先比較ob_size,如果ob_size不一樣則可以直接比較出大小來。顯然ob_size越大,對應的整數越大,不管ob_size是正是負,都符合這個結論,可以想一下。

整數2 ^ 30 -1:

如果想表示2 ^30 - 1(^這里代指冪運算,當然對於Python程序猿來說兩個星號也是冪運算,表達的意義是一樣的),那么也可以使用一個digit表示。雖然如此,但為什么突然舉2 ^ 30 - 1這個數字呢?答案是,雖然digit是4字節、32位,但是Python只用30個位。

之所以這么做是和加法進位有關系,如果32個位全部用來存儲其絕對值,那么相加產生進位的時候,可能會溢出,比如有一個將32個位全部占滿的整數(2 ^ 32 - 1),即便它只加上1,也會溢出。這個時候為了解決這個問題,就需要先強制轉換為64位再進行運算。

但如果只用30個位的話,那么加法是不會溢出的,或者說相加之后依舊可以用32位整數保存。因為30個位最大就是2 ^ 30 - 1,即便兩個這樣的值相加,結果也是(2 ^ 30 - 1) * 2,即:2 ^ 31 - 2。而32個位的話最大是2 ^ 32 - 1,所以肯定不會溢出的;如果一開始30個位就存不下,那么數組中會有兩個digit。

所以雖然將32位全部用完,可以只用一個digit表示更多、更大的整數,但是可能面臨相加之后一個digit存不下的情況,於是只用30個位,如果數值大到30個位存不下的話,那么就會多使用一個digit。可能有人發現了,如果是用31個位的話,那么相加產生的最大值就是2 ^ 32 - 2,結果依舊可以使用一個32位整型存儲啊,那Python為啥要犧牲兩個位呢?答案是為了乘法運算。

// 還記得這個宏嗎?PYLONG_BITS_IN_DIGIT指的就是Python使用digit的位數
// 我們看到在32位機器上,digit相當於2字節、16位的整型,而它用了15位,只犧牲了一個位
// 64 位機器上則犧牲兩個位
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
// ...
#endif

整數2 ^ 30:

問題來了,我們說digit只用30位,所以2 ^ 30 - 1是一個digit能存儲的最大值,那么現在是2 ^ 30,所以數組中就要有兩個digit了。

我們看到此時就用兩個digit來存儲了,此時的數組里面的元素就是0和1,而且充當高位的放在后面,因為我們說了使用新的digit來模擬更高的位。由於一個digit只用30位,那么數組中第一個digit的最低位就是1,第二個digit的最低位就是31,第三個digit的最低位就是61,以此類推,所以如果ob_digit為[a, b, c],那么對應的整數就為: a * 2 ** 0 + b * 2 ** 30 + c * 2 ** 60,如果ob_digit不止3個,那么就按照30個位往上加,比如ob_digit還有第四個元素d,那么就再加上d * 2 ** 90即可。

再比如我們反推一下,如果a = 88888888888,那么底層數組ob_digit中的值是多少?

import numpy as np

a = 88888888888
# 我們說1個digit用30個位, 那么n個digit所能表示的最大整數就是2 ** (30 * n) - 1, 至於原因的話其實很好理解,但我們還是可以嚴格推導一下
# 我們以n = 2為例, 顯然兩個digit最高能表示 (2 ** 30 - 1) + (2 ** 30 - 1) * 2 ** 30,
# 它等於 (2 ** 30 - 1) + (2 ** 60 - 2 ** 30) = 2 ** 60 - 1, 因此兩個digit最大可以表示 2 ** 60 - 1
# 同理你可以n取3, 看看(2 ** 30 - 1) + (2 ** 30 - 1) * 2 ** 30 + (2 ** 30 - 1) * 2 ** 60是不是等於2 ** 90 - 1
# 或者試試更大的數, 結論都是成立的
print(np.log2(a))  # 36.37128404230425
# 36超過了30個位、但小於90個位, 因此需要兩個digit

# 我們說 "整數 = ob_digit[0] + ob_digit[1] * 2 ** 30 + ob_digit[2] * 2 ** 60 + ..."
# 但是對於ob_digit長度為2的情況下, 這里的a = ob_digit[0] + ob_digit[1] * 2 ** 30
print(a // 2 ** 30)  # 82
print(a - 82 * 2 ** 30)  # 842059320

# 說明此時底層對應的ob_digit數組就是[842059320, 82]

我們修改解釋器源代碼重新編譯,通過在創建整數的時候打印ob_digit里面的元素的值,也印證了這個結論。

這個時候,我們可以分析整數所占的字節了。相信所有人都知道可以使用sys.getsizeof計算大小,但是這大小到底是怎么來的,估計會一頭霧水。因為Python中對象的大小,是根據底層的結構體計算出來的。

我們說ob_refcnt、ob_type、ob_size這三個是整數所必備的,它們都是8字節,加起來24字節。所以任何一個整數所占內存都至少24字節,至於具體占多少,則取決於ob_digit里面的元素都多少個。

因此Python中整數所占內存 = 24 + 4 * ob_digit數組長度

import sys

# 如果是0的話, ob_digit數組為空, 所以此時就是24字節
print(sys.getsizeof(0))  # 24

# 如果是1的話, ob_digit數組有一個元素, 所以此時是24 + 4 = 28字節
print(sys.getsizeof(1))  # 28
print(sys.getsizeof(2 ** 30 - 1))  # 28

# 一個digit只用30位, 所以最大能表示2 ** 30 - 1
# 如果是2 ** 30, 那么就需要兩個元素, 所以是24 + 4 * 2 = 32字節
# 如果是兩個digit, 那么能表示的最大整數就是2 ** 60 - 1
print(sys.getsizeof(2 ** 30))  # 32
print(sys.getsizeof(2 ** 60 - 1))  # 32


"""
相信下面的不需要解釋了
"""
print(sys.getsizeof(1 << 60))  # 36
print(sys.getsizeof((1 << 90) - 1))  # 36

print(sys.getsizeof(1 << 90))  # 40

小整數對象池

由於分析過了浮點數以及浮點類型對象,因此int類型對象的實現以及int實例對象的創建啥的就不說了,可以自己去源碼中查看,我們后面會着重介紹它的一些操作。還是那句話,Python中的API設計的很優美,都非常的相似,比如創建浮點數可以使用PyFloat_FromDouble、PyFloat_FromString等等,那么創建整數也可以使用PyLong_FromLong、PyLong_FromDouble、PyLong_FromString等等,直接去Objects中對應的源文件中查看即可。

這里說一下Python中的小整數對象池,我們知道Python中的整數屬於不可變對象,運算之后會創建新的對象。

>>> a = 666
>>> id(a)
2431274354736
>>> a += 1
>>> id(a)
2431274355024
>>>

所以這種做法就勢必會有性能缺陷,因為程序運行時會有大量對象的創建和銷毀。根據浮點數的經驗,我們猜測Python應該也對整數使用了緩存池吧。答案是差不多,只不過不是緩存池,而是小整數對象池。

Python將那些使用頻率高的整數預先創建好,而且都是單例模式,這些預先創建好的整數會放在一個靜態數組里面,我們稱為小整數對象池。如果需要使用的話會直接拿來用,而不用重新創建。注意:這些整數在Python解釋器啟動的時候,就已經創建了。

所以這種做法就勢必會有性能缺陷,因為程序運行時會有大量對象的創建和銷毀。根據浮點數的經驗,我們猜測Python應該也對整數使用了緩存池吧。答案是差不多,只不過不是緩存池,而是小整數對象池。小整數對象池的實現位於longobject.c中。

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
  • NSMALLPOSINTS宏規定了對象池中正數的個數 (從 0 開始,包括 0 ),默認 257 個;
  • NSMALLNEGINTS宏規定了對象池中負數的個數,默認5個;
  • small_ints是一個整數對象數組,保存預先創建好的小整數對象;

以默認配置為例,Python解釋器在啟動的時候就會預先創建一個可以容納262個整數的數組,並會依次初始化 -5 到 256(包括兩端)之間的262個PyLongObject。所以小整數對象池的結構如下:

但是為什么要實現緩存從-5到256之間的整數呢?因為Python認為這個范圍內的整數使用頻率最高,而緩存這些整數的內存相對可控。因此這只是某種權衡,很多程序的開發場景都沒有固定的正確答案,需要根據實際情況來權衡利弊。

>>> a = 256
>>> b = 256
>>> id(a), id(b)
(140714000246400, 140714000246400)
>>>
>>> a = 257
>>> b = 257
>>> id(a), id(b)
(2431274355184, 2431274354896)
>>>

256位於小整數對象池內,所以全局唯一,需要使用的話直接去取即可,因此它們的地址是一樣的。但是257不再小整數對象池內,所以它們的地址不一樣。

我們上面是在交互式下演示的,但如果有小伙伴不是通過交互式的話,那么會得到出乎意料的結果。

a = 257
b = 257
print(id(a) == id(b))  # True

可能有人會好奇,為什么地址又是一樣的了,257明明不在小整數對象池中啊。雖然涉及到了后面的內容,但是提前解釋一下也是可以的。主要區別就在於一個是在交互式下執行的,另一個是通過 python3  xxx.py的方式執行的。

首先Python的編譯單元是函數,每個函數都有自己的作用域,在這個作用域中出現的所有常量都是唯一的,並且都位於常量池中,由co_consts指向。雖然我們上面的不是函數,而是在全局作用域中,但是全局你也可以看成是一個函數,它也是一個獨立的編譯單元。同一個編譯單元中,常量只會出現一次。

當a = 257的時候,會創建257這個整數、並放入常量池中;所以b = 257的時候就不會再創建了,因為常量池中已經有了,所以會直接從常量池中獲取,因此它們的地址是一樣的,因為是同一個PyLongObject。

# Python3.6下執行, 該系列的所有代碼都是基於Python3.8, 但是這里先使用Python3.6, 至於原因, 后面會說
def f1():
    a = 256
    b = 257
    return id(a), id(b)


def f2():
    a = 256
    b = 257
    return id(a), id(b)


print(f1())  # (140042202371968, 140042204149712)
print(f2())  # (140042202371968, 140042204255024)

此時f1和f2顯然是兩個獨立的編譯單元,256屬於小整數對象池中的整數、全局唯一,因此即便不在同一個編譯單元的常量池中,它的地址也是唯一的,因為它是預先定義好的,所以直接拿來用。但是257顯然不是小整數對象池中的整數,而且不在同一個編譯單元的常量池中,所以地址是不一樣的。

而對於交互式環境來說,因為我們輸入一行代碼就會立即執行一行,所以任何一行可獨立執行的代碼都是一個獨立的編譯單元。注意:是可獨立執行的代碼,比如變量賦值、函數、方法調用等等;但如果是if、for、while、def等等需要多行表示的話,比如:if 2 > 1:,顯然這就不是一行可獨立執行的代碼,它還依賴你輸入的下面的內容。

>>> if 2 > 1:  # 此時按下回車,我們看到不再是>>>, 而是..., 代表還沒有結束, 還需要你下面的內容
...     print("2 > 1")
...  # 此時這個if語句整體才是一個獨立的編譯單元
2 > 1
>>>

但是像a = 1、foo()、lst.appned(123)這些顯然它們是一行可獨立執行的代碼,因此在交互式中它們是獨立的編譯單元。

>>> a = 257  # 此時這行代碼已經執行了,它是一個獨立的編譯單元
>>> b = 257  # 這行代碼也是獨立的編譯單元,所以它里面的常量池為空,因此要重新創建
>>> id(a), id(b)  # 由於它們是不同常量池內的整數,所以id是不一樣的。
(2431274355184, 2431274354896)

但是問題來了,看看下面的代碼,a和b的地址為啥又一樣了呢?666和777明顯也不在常量池中啊。

>>> a = 666;b=666
>>> id(a), id(b)
(2431274354896, 2431274354896)
>>> a, b = 777, 777
>>> id(a), id(b)
(2431274354800, 2431274354800)
>>>

顯然此時應該已經猜到原因了,因為上面兩種方式無論哪一種,都是在同一行,因此整體會作為一個編譯單元,所以地址是一樣的。

def f1():
    a = 256
    b = 2 ** 30
    return id(a), id(b)


def f2():
    a = 256
    b = 2 ** 30
    return id(a), id(b)


print(f1())  # (140714000246400, 2355781138896)
print(f2())  # (140714000246400, 2355781138896)

但是在Python3.8中,如果是通過 python xxx.py的方式執行的話,即便是大整數、並且不再同一個編譯單元的常量池中,它們的地址也是一樣的,說明Python在3.8版本的時候做了優化。

另外,如果沒有特殊說明,那么我們這個系列的所有代碼都是在Python3.8下執行的。說實話,我就是因為發現在Python3.8中,打印的地址都是一樣的,才在上面試了一下Python3.6。但是Python3.8中具體是怎么優化的,這里就暫時不討論了(明明是你沒有仔細研究)

整數運算

整數溢出是程序開發中一大難題,由此引發的 BUG 不計其數,而且相當隱蔽。之前使用golang刷LeetCode的時候,怎么也通不過,最后發現是因為LeetCode后台有一個測試用例比較特殊,導致整數太大,golang中的int64存不下。而Python 選擇從語言層面徹底解決這個痛點,殫心竭慮地設計了整數對象。而我們也探索了整數對象,並初步掌握了整數對象的內部結構。

Python中的整數是串聯了多個C中的digit(uint32_t),通過一個C數組的形式來實現整數的表示。這么做的好處就是Python中的整數沒有長度限制了,因此不會溢出(而浮點數使用C的double,所以它會溢出)。之所以不會溢出,是因為數組是沒有長度限制的,所以只要你的內存足夠,就可以算任意大的數。所以Python表示:存不下?會溢出?這都不是事兒,直接繼續往數組里面塞digit就ok了。

這里再重溫一下PyLongObject的數據結構,我們說它是一個變長對象。ob_size指的是數組的長度,並且它除了表示長度還能體現出整數的正負,而ob_digit這個數組只用來存儲其絕對值。

但是說實話,用整數數組實現大整數的思路其實平白無奇,但難點在於大整數 數學運算 的實現,它們才是重點,也是也比較考驗編程功底的地方。

所以我們在分析浮點數的時候,一直說整數要比浮點數復雜,原因就在於此。浮點數相加的話直接兩個double相加即可,但是整數相加可就沒有那么簡單了。

整數支持的操作定義在什么地方相信不用我說了,直接去longobject.c中查看就可以了,根據浮點數的經驗我們知道PyLong_Type中的tp_as_number成員也指向了PyNumberMethods結構體實例,里面的成員都是指向與整型運算相關的函數的指針。

注意:圖中有一個箭頭畫錯了,應該是 ob_type 指向 PyLong_Type,但圖中不小心變成了 ob_size。

整數的大小比較

先來看看Python中的整數在底層是如何比較的吧。

static int
long_compare(PyLongObject *a, PyLongObject *b)
{	
    //sign是一個8字節的long, 用來表示a和b之間的比較結果
    //如果a == b, 那么sign = 0; 如果a > b, 那么sign > 0; 如果a < b, 那么sign < 0
    Py_ssize_t sign;
	
    //Py_SIZE是一個宏:獲取對象的ob_size,除此之外我們之前還見到了Py_REFCNT和Py_TYPE, 用來獲取對象的引用計數和類型指針
    //如果兩個整數的ob_size不一樣, 我們說a和b一定不相等, 所以可以直接比較出大小
    if (Py_SIZE(a) != Py_SIZE(b)) {
        //如果一正一負, 那么肯定正的大, 因為ob_size還體現整數的正負, 所以正的ob_size對應的整數也會更大
        //如果都為正, 那么ob_size越大, 對應數組元素就越多, 顯然整數就越大
        //如果都為負, 那么ob_size越大, 其絕對值就越小, 因為越接近0,所以對應的整數的絕對值也越小
          //但因為是負數,所以乘上-1之后,所以整數值反而會越大。比如: 1 < 100, 但是乘上-1之后, 小於號就要變成大於號
        //因此無論是哪種情況,如果兩個整數的ob_size不一樣,是可以直接比較出大小的。
        sign = Py_SIZE(a) - Py_SIZE(b);
        //所以sign > 0的話a > b, sign < 0的話a < b, 因為ob_size不一樣, 所以sign不可能等於0
    }
    else {
        //如果相等, 那么說明a和b的符號相同, 數組中使用的digit也是一樣的
        //那么接下來就只能挨個比較數組中的digit了
        //這里是獲取數組的長度, 賦值給變量i
        Py_ssize_t i = Py_ABS(Py_SIZE(a));
        //我們之前說,一個digit存不下,那么會使用兩個digit, 以此類推
        //並且代表整數高位的digit會放在后面, 而比較兩個數的大小顯然是從高位開始比
        //因此遍歷數組是從后往前遍歷的, 先比較a -> ob_digit[n]和 b -> ob_digit[n]
        //如果一樣就比較a -> ob_digit[n-1]和a -> ob_digit[n-1],直到將數組的元素全部比完,顯然只要有一個不一樣,就可以直接決定絕對值的大小
        while (--i >= 0 && a->ob_digit[i] == b->ob_digit[i])
            //進行while循環, i是數組的長度, 因此數組的最大索引是i - 1, 所以這里的--i會先將i自減1,再判斷自減1之后的i是否>=0
            //然后比較a->ob_digit[i]和b->ob_digit[i], 如果數組內元素全部一樣, 那么循環結束之后i肯定是-1,只要有一個不一樣, 那么i一定>=0
            ;
        if (i < 0)
            //所以如果i < 0,說明兩個整數的數組全部一樣, 因此兩個整數是一樣的
            //所以sign = 0
            sign = 0;
        else {
            //否則的話, 說明數組中索引為i的元素不一樣, 那么直接相減就可以了
            //如果sign大於0, 顯然a對應的絕對值比大, 否則a對應的絕對值比b小
            sign = (sdigit)a->ob_digit[i] - (sdigit)b->ob_digit[i];
            if (Py_SIZE(a) < 0)
                //但是我們說計算的是絕對值,如果ob_size小於0,絕對值越大其值反而越小,那么sign還要乘上-1
                sign = -sign;
        }
    }
    //因此最終: a > b則sign > 0, a < b則sign < 0, a == b則sign == 0
    //然后這里是一個嵌套的三元表達式, sign大於0則直接返回1表示a > b, 小於0返回-1表示a < b, 等於0則返回0表示a == b
    return sign < 0 ? -1 : sign > 0 ? 1 : 0;
}

所以我們看到Python中的整數就是按照上面這種方式比較的,總的來說就是先比較ob_size,ob_size不一樣則可以直接比較。如果ob_size一樣的話,那么會從后往前挨個比較數組中的元素,最終確定大小關系。

整數的相加

再來看看Python中的整數在底層是如何相加的,加法的實現顯然是long_add,我們看一下。

static PyObject *
long_add(PyLongObject *a, PyLongObject *b)
{	
    //a和b是兩個PyLongObject *
    //z顯然是指向a和b相加之后的PyLongObject
    PyLongObject *z;
	
    //CHECK_BINOP是一個宏, 接收兩個指針, 檢測它們是不是都指向PyLongObject
    CHECK_BINOP(a, b);
	
    //判斷a和b的ob_size的絕對值是不是都小於等於1, 如果是的話, 那么說明數組中最多只有一個元素
    //數組沒有元素,說明整數是0;有一個元素,那么直接取出來、再判斷正負號即可,然后直接相加。
    //所以顯然這里走的是快分支,因為絕對值超過2 ** 30 - 1的整數還是比較少的
    if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
        //MEDIUM_VALUE是一個宏, 接收一個abs(ob_size) <= 1的PyLongObject的指針
        //如果ob_size是0, 那么結果為0; 如果ob_size絕對值為1, 那么結果為 ob_digit[0] 或者 -ob_digit[0]
        //所以直接將MEDIUM_VALUE(a) + MEDIUM_VALUE(b)之后的結果轉成PyLongObject,然后返回其指針即可
        //因此如果數組中元素不超過1個的話, 那么顯然是可以直接相加的
        return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b));
    }
    //走到這里, 說明至少有一方的ob_size大於1
    //如果a < 0
    if (Py_SIZE(a) < 0) {
        //如果a < 0並且b < 0
        if (Py_SIZE(b) < 0) {
            //說明兩者符號相同, 那么通過x_add直接將兩個整數相加即可
            //這個x_add專門用於整數的絕對值相加,並且會返回PyLongObject *,它的實現我們后面會說
            //所以z指向的PyLongObject的內部成員是已經設置好了的
            //只不過x_add加的是兩者的絕對值, z指向的PyLongObject內部ob_type的符號我們還需要再度判斷一下
            z = x_add(a, b);
            if (z != NULL) {
                assert(Py_REFCNT(z) == 1);
                //因為a和b指向的整數都是負數, 所以將相加之后還要將ob_size乘上-1
                Py_SIZE(z) = -(Py_SIZE(z));
            }
        }
        else
            //走到這里說明a < 0並且b > 0, 那么直接讓b - a即可, 此時得到的結果一定是正
            //因此不需要考慮ob_size的符號問題
            z = x_sub(b, a);
    }
    else {
        //走到這里說明a > 0並且b < 0, 所以讓a - b即可
        if (Py_SIZE(b) < 0)
            z = x_sub(a, b);
        else
            //此時兩個整數均為正, 直接相加
            z = x_add(a, b);
    }
    //返回z的指針
    return (PyObject *)z;
}

所以long_add這個函數並不長,但是調用了輔助函數x_add和x_sub,顯然核心邏輯是在這兩個函數里面。至於long_add函數,它的邏輯如下:

  • 1. 定義一個變量z, 用於保存計算結果;
  • 2. 判斷兩個整數底層對應的數組是不是都不超過1, 如果是的話那么通過宏MEDIUM_VALUE直接將其轉成C中的一個digit, 然后直接相加、返回即可。顯然這里走的是快分支,或者快速通道;
  • 3. 但如果有一方ob_size絕對值不小於1, 那么判斷兩者的符號。如果都為負,那么通過x_add計算兩者絕對值之和、再將ob_size乘上-1即可;
  • 4. 如果a的ob_size小於0, b的ob_size大於0, 那么通過x_sub計算b和a絕對值之差即可;
  • 5. 如果a的ob_size大於0, b的ob_size小於0, 那么通過x_sub計算a和b絕對值之差即可;
  • 6. 如果a的ob_size大於0, b的ob_size大於0, 那么通過x_add計算讓b和a絕對值之和即可;

所以Python中整數的設計非常的巧妙,ob_digit雖然是用來維護具體數值,但是它並沒有考慮正負,而是通過ob_size來表示整數的正負號。這樣運算的時候,計算的都是整數的絕對值,因此實現起來會方便很多。將絕對值計算出來之后,再通過ob_size來判斷正負號。

因此long_add將整數加法轉成了 "絕對值加法(x_add)"和"絕對值減法(x_sub)":

  • x_add(a, b), 計算兩者的絕對值之和, 即:|a| + |b|;
  • x_sub(a, b), 計算兩者的絕對值之差, 即:|a| - |b|;

由於絕對值加、減法不用考慮符號對計算結果的影響,實現更為簡單,這是Python將整數運算轉化成絕對值運算的緣由。雖然我們還沒看到x_add和x_sub是如何對整數的絕對值進行相加和相減運算的,但也能從中體會到程序設計中邏輯的 划分 與 組合 的藝術,優秀的代碼真的很美。

那么下面我們的重心就在x_add和x_sub中了,看看它們是如何對大整數絕對值進行運算的。但是你可能會有疑問,大整數運算肯定很復雜,效率會差吧。顯然這是必然的,整數數值越大,整數對象底層數組越長,運算開銷也就越大。好在運算處理函數均以快速通道對小整數運算進行優化,將額外開銷降到最低。

比如上面的long_add,如果a和b對應的整數的絕對值都小於等於2 ^ 30 - 1,那么會直接轉成C中的整型進行運算,性能損耗極小。並且走快速通道的整數的范圍是:-(2 ^ 30 - 1) ~ 2 ^ 30 - 1,即:-1073741823 ~ 1073741823,顯然它可以滿足我們絕大部分的運算場景。

絕對值加法x_add:

在介紹絕對值加法之前,先來看看幾個宏,先不管它們是干什么的,會在x_add中有體現:

#define PyLong_SHIFT    30
#define PyLong_BASE     ((digit)1 << PyLong_SHIFT)
#define PyLong_MASK     ((digit)(PyLong_BASE - 1))
//所以PyLong_MASK等於(1 << 30) - 1, 就等於2 ** 30 - 1, 說明32個位, 前兩個位為0, 后面三十個位則都是1

此外,再想象一下我們平時算加法的時候是怎么算的:

而x_add在邏輯和上面是類似的,下面分析x_add的實現:

static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{	
    //顯然a和b指向了兩個要想加的整數對象
    //這里獲取a和b的ob_size的絕對值
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    //根據a和b的相加結果所創建的新的PyLongObject的指針
    PyLongObject *z;
    //循環變量
    Py_ssize_t i;
    //重點也是最難理解的地方: carry用於每個部分的運算結果(可不是大神帶你carry哦)
    digit carry = 0;
	
    //如果size_a小於size_b
    if (size_a < size_b) {
        //那么將a和b進行交換, 以及size_a和size_b也進行交換, 為什么這么做呢?因為方便
        //我們小時候計算兩個整數相加時候, 如果一個位數多,一個位數少, 也會習慣將位數多的放在左邊
        //最終從右往左, 也就是從低位往高位逐個相加, 大於10則進1
        { PyLongObject *temp = a; a = b; b = temp; }
        { Py_ssize_t size_temp = size_a;
            size_a = size_b;
            size_b = size_temp; }
        //如果size_a和size_b相等, 或者size_a大於size_b, 那么該if就無需執行了
    }
    //這里是創建一個ob_size為size_a + 1的PyLongObject, 然后返回其指針
    z = _PyLong_New(size_a+1);
    //但為什么是size_a + 1呢?
    //因為此時size_a 一定不小於 size_b, 那么a和b相加之后的z的ob_size一定不小於size_a
    //但是也可以也可能比size_a多1, 比如: a = 2 ** 60 - 1, b = 1
    //所以相加之后結果為2 ** 60次方, 所以ob_size就變成了3, 因此在創建z的時候,ob_digit的容量會等於size_a + 1
    
    //正常情況下, z是一個PyLongObject *, 但如果z == NULL, 表示分配失敗(解釋器也會異常退出)
    //但說實話, 除非你內存不夠了, 否則這種情況不會發生
    if (z == NULL)
        return NULL;
    
    //重點來了, 如果a和b的ob_size不一樣, 那么size_a會大於size_b
    //所以顯然是先以size_b為准, 兩者從低位向高位依次對應相加; 當b到頭了, 再單獨算a的剩余部分;
    //假設size_a == 4, size_b == 2, 對應到ob_digit的話
    //就是a -> ob_digit[0] + b -> ob_digit[0], 作為z -> ob_digit[0], 當然還需要考慮進位, 下面說
    //然后a -> ob_digit[1] + b -> ob_digit[1], 作為z -> ob_digit[1], 此時b到頭了
    //繼續a -> ob_digit[2]作為z -> ob_digit[2], a -> ob_digit[3]作為z -> ob_digit[3]
    //此時a和b相加就結束了, 如果不考慮相加進位的話, 那么整體流程就是這個樣子。然后我們繼續往下看
    
    //從索引為0開始遍歷, 以i < size_b為條件
    for (i = 0; i < size_b; ++i) {
        //將a->ob_digit[i] + b->ob_digit[i]作為carry, 顯然carry如果沒有超過2 ** 30 - 1的話
        //顯然它就是z -> ob_digit[i]
        carry += a->ob_digit[i] + b->ob_digit[i];
        //但是carry是可能溢出的, 所以z -> ob_digit[i] = carry & PyLong_MASK
        //這個PyLong_MASK就是我們在介紹x_add之前先介紹的幾個宏之一, 它表示的是2 ** 30 - 1
        //我們說它的前兩個位為0, 后面三十個位全是1, 因此對於后面三十個位來說, 在和carry進行"與運算"之后,對應的位還和carry保持一致
        //所以在carry小於等於2 ** 30 - 1的時候carry & PyLong_MASK就等於carry
        //但如果carry大於2 ** 30 - 1, 由於PyLong_MASK的前兩位為0, 所以這一步可以確保carry不會超過2 ** 30 - 1
        z->ob_digit[i] = carry & PyLong_MASK;
        //但是carry的前兩位顯然不可以丟, 所以它們要作用在數組中下一個元素相加的結果上
        //比如a -> ob_digit[0] + b -> ob_digit[0]得到結果正好是2 ** 32 - 1, 那么carry的前兩位也是1
        //而數組中下一元素相加之后, 其結果對應的位要比本次循環高出30
        //所以這里將carry右移30位, 然后作用到下一次循環中
        carry >>= PyLong_SHIFT;
    }
    for (; i < size_a; ++i) {
        //如果當b到頭了, 那么繼續從當前的i開始, 直到i == size_a, 邏輯還是和上面一樣的
        //只不過將a->ob_digit[i] + b->ob_digit[i]換成了a->ob_digit[i], 因為b到頭了
        carry += a->ob_digit[i];
        //這里也要"與上"PyLong_MASK, 因為也可能存在進位的情況, 拿生活中的99999 + 1為例
        //此時a = 99999, b = 1, 顯然第一次循環b就到頭了, 但后面單獨循環a的時候, 依舊是要加進位的
        //所以這里也是同理
        z->ob_digit[i] = carry & PyLong_MASK;
        //carry右移30位
        carry >>= PyLong_SHIFT;
    }
    //兩個循環結束之后, 其實還差一步, 還拿99999 + 1舉例子, 按照順序相加最后得到的是00000
    //因為最后還進了一個1, 所以這里的carry也是同理, 因此z的ob_size要比size_a多1, 目的就在於此
    z->ob_digit[i] = carry;
    //但如果最后的carry沒有進位的話, 顯然其結果就是0, 所以最后沒有直接返回z, 而是返回了long_normalize(z)
    //這個long_normalize函數作用就是從后往前依次檢查ob_digit的元素, 如果為0, 那么就將其ob_size減去1, 直到出現一個不為0的元素
    //當然對於我們當前來說, 顯然最多只會檢查一次
    return long_normalize(z);
}

Python中的整數在底層實現的很巧妙,不理解的話可以多看幾遍,然后我們在Python的層面上再反推一下,進一步感受底層運算的過程。

# 假設有a和b兩個整數, 當然這里是使用列表直接模擬的底層數組ob_digit
a = [1073741744, 999, 765, 123341]
b = [841, 1073741633, 2332]
# 然后創建z, 表示a和b的相加結果
z = []

# 為了更直觀, 我們一步步手動相加
# 首先是將a[0] + b[0], 得到carry
carry = a[0] + b[0]
# 然后carry & (2 ** 30 - 1), 我們看到結果是761
print(carry & (2 ** 30 - 1))  # 761
# 因為如果carry小於等於 2 ** 30 - 1, 那么結果就是carry, 而這里是761, 顯然carry肯定大於 2 ** 30 - 1
print(carry > 2 ** 30 - 1)  # True
# 因為carry & (2 ** 30 - 1) == 761, 所以z的第一個元素就是761
z.append(761)


# 然后計算a[1] + b[1]得到新的carry, 但是之前的carry大於 2 ** 30 - 1
# 所以還要再加上之前的右移30位的carry
carry = (carry >> 30) + a[1] + b[1]
# 然后carry & (2 ** 30 - 1)得到809
print(carry & (2 ** 30 - 1))  # 809
# 說明carry依舊大於 2 ** 30 - 1
print(carry > 2 ** 30 - 1)  # True
# 然后z的第二個元素就是809
z.append(809)


# 計算a[2] + b[2]的時候也是同理
carry = (carry >> 30) + a[2] + b[2]
# 但是顯然此時的carry已經不大於 2 ** 30 - 1了
print(carry > 2 ** 30 - 1)  # False
# 所以carry和carry & (2 ** 30 - 1)的結果都是carry本身
print(carry, carry & (2 ** 30 - 1))  # 3098 3098
# 說明z的第三個元素是3098
z.append(3098)

# 此時b到頭了, 所以直接將a[3]作為carry, 當然我們不知道carry是否大於2 ** 30 - 1
# 所以還是右移30位即可, 不過carry不大於2 ** 30 - 1的話, 那么 carry >> 30 就是0罷了
carry = (carry >> 30) + a[3]
print(carry)  # 123341
# 說明z的最后一個元素是123341, 當然理論上我們還要在對carry和 2 ** 30 - 1進行一次判斷
# 當然由於我們知道carry肯定不會超過2 ** 30 - 1, 所以就不判斷了
z.append(123341)

# 此時z為[761, 809, 3098, 123341]
print(z)  # [761, 809, 3098, 123341]

# 所以ob_digit為[1073741744, 999, 765, 123341]和[841, 1073741633, 2332]的兩個PyLongObject相加
# 得到的新的PyLongObject的ob_digit為[761, 809, 3098, 123341]

# 我們根據ob_digit按照規則轉成整數, 那么a + b的結果要和z是相等的
a = 1073741744 + 999 * 2 ** 30 + 765 * 2 ** 60 + 123341 * 2 ** 90
b = 841 + 1073741633 * 2 ** 30 + 2332 * 2 ** 60
z = 761 + 809 * 2 ** 30 + 3098 * 2 ** 60 + 123341 * 2 ** 90
print(a)  # 152688762386380073438430860672944
print(b)  # 2689765870042689307465
print(z)  # 152688762389069839308473549980409

# 顯然結果為True, 由此證明我們之前的結論是成立的。
print(a + b == z)  # True

看完絕對值加法x_add之后,再來看看絕對值減法x_sub,顯然有了加法的經驗之后再看減法會簡單很多。

static PyLongObject *
x_sub(PyLongObject *a, PyLongObject *b)
{	
    //依舊是獲取兩者的ob_size的絕對值
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    //z指向相加之后的PyLongObject
    PyLongObject *z;
    //循環變量
    Py_ssize_t i;
    //如果size_a 小於 size_b, 那么sign就是-1, 否則就是1
    int sign = 1;
    //之前carry保存的相加的結果, borrow保存相減的結果
    //名字很形象, 相加要進位叫carry、相減要結尾叫borrow
    digit borrow = 0;

    //如果size_a比size_b小, 說明a的絕對值比b小
    if (size_a < size_b) {
        //那么令sign = -1, 相減之后再乘上sign
        //因為計算的是絕對值之差, 符號是在絕對值之差計算完畢之后通過sign判斷的
        sign = -1;
        //然后依舊交換兩者的位置, 相減的時候也確保大的一方在左邊
        //相加的時候其實大的一方在左邊還是在右邊沒有太大影響, 但是相減的時候大的一方在左邊顯然會省事很多
        //但是交換之后再相減, 肯定要變符號, 因此將sign設置為-1
        { PyLongObject *temp = a; a = b; b = temp; }
        { Py_ssize_t size_temp = size_a;
            size_a = size_b;
            size_b = size_temp; }
        //可能有人會有疑問了,那如果a的ob_size是1, b的ob_size是-3,一正一負,此時起到的效果是相加才對啊
        //是的, 所以此時會將a和b傳到x_add里面,而不是這里, 后面我們會總結
        //由於ob_digit里面的元素都為正, 所以x_add計算的是絕對值之和,x_sub計算的絕對值之差, 總之在理解邏輯的時候把a和b都想象成正數即可
    }
    else if (size_a == size_b) {
        //這一個條件語句可能有人會覺得費解,我們分析一下
        //如果兩者相等, 那么兩個ob_digit里面對應的元素也是有幾率都相等的
        i = size_a;
        //所以從ob_digit的尾巴開始遍歷
        while (--i >= 0 && a->ob_digit[i] == b->ob_digit[i])
            ;
        //如果都相等, 那么i會等於-1
        if (i < 0)
            //所以直接返回0即可
            return (PyLongObject *)PyLong_FromLong(0);
        //下面下面是為了計算相減之后的PyLongObject的ob_size
        //如果對應元素不相等, 假設a的ob_digit里面的元素是[2, 3, 4, 5], b的ob_digit是[1, 2, 3, 5]
        //因此上面的while循環結束之后, i會等於2, 顯然只需要計算[2, 3, 4]和[1, 2, 3]之間的差即可, 因為最高位的5是一樣的
        //然后判斷索引為i時, 對應的值誰大誰小
        if (a->ob_digit[i] < b->ob_digit[i]) {
            //如果a->ob_digit[i] < b->ob_digit[i], 那么同樣說明a小於b, 因此將sign設置為-1, 然后交換a和b的位置
            sign = -1;
            { PyLongObject *temp = a; a = b; b = temp; }
        }
        //因為做減法, 所以size_a和size_b直接設置成i + 1即可, 因為高位在減法的時候會被抵消掉, 所以它們完全可以忽略
        size_a = size_b = i+1;
    }
    //這里依舊是申請空間
    z = _PyLong_New(size_a);
    //申請失敗返回NULL
    if (z == NULL)
        return NULL;
    
    //然后下面的邏輯和x_add是類似的
    for (i = 0; i < size_b; ++i) {
        //讓a->ob_digit[i] - b->ob_digit[i], 但如果存在借位, 那么還要減掉
        //但是問題來了, 我們說digit貌似是無符號的吧, 但是對於低位來說a->ob_digit[i] 是完全可以小於 b->ob_digit[i]的
        //但是這樣減出來不成負數了, 所以C語言中有這么個特點, 比如:這里相減得到的是-100
        //那么結果就是2 ** 32 - 100, 因為digit是無符號32位, 所以存儲的負數會變成 2 ** 32 + 該負數, 或者2 ** 32 - 負數的絕對值
        //以我們平時做的減法為例:32 - 19, 我們知道結果是13, 但是低位的2減去低位的9結果是-7, 如果是負數
        //那么要像高位借個1, 從而得到10,因此最后一位是10 - 7 = 3
        //以此為例, a -> ob_digit[i] - b -> ob_digit[i], 如果小於0, 那么肯定要像數組中i + 1的元素進行借位, 但我們說它會比當前高30個位
        borrow = a->ob_digit[i] - b->ob_digit[i] - borrow;
        //因此這里借個1, 借的就不是10了, 而是2 ** 30次方
        //所以borrow為負, 那么結果顯然加上2 ** 30才對, 但是當前borrow加的是2 ** 32次方
        //所以將borrow 還要 與上 PyLong_MASK,然后其結果才是z->ob_digit[i]的值
        z->ob_digit[i] = borrow & PyLong_MASK;
        //如果真的借了個1, 那么ob_digit中下一個元素肯定是要減去1的, 所以borrow右移30位
        borrow >>= PyLong_SHIFT;
        //和1進行與運算, 如果a -> ob_digit[i] - b -> ob_digit[i]為負, 那么就必須要借位
        //但由於digit只用30個位, 因此再加上2 ** 32次方之后,其結果的第31位一定是1
        //所以borrow右移30位之后, 再和1進行與運算之后結果肯定是1, 由此可以判斷這次相減一定是借位了
        //如果為0代表結果為正、沒有加上2 ** 32次方,那么結果borrow & 1的結果就是0
        borrow &= 1; 
        //所以Python底層的整數只用了30個位真的非常巧妙, 尤其是在減法的時候
        //因為借位一次借2 ** 30, 可由於C的特性會加上2 ** 32次方, 但是它們的結果只有前兩個高位不一樣, 后面30個位是一樣的
        //所以再與上PyLong_MASK, 所以就等價於加上了2 ** 30次方,從而得到正確的結果
        //但如果一旦借位, 那么數組下一個元素要減去1。但問題是怎么判斷它有沒有借位呢?判斷有沒有借位就是判斷兩個元素相減之后是否為負
        //如果為負數,那么C會將這個負數加上2 ** 32次方, 而兩個不超過2 ** 30 - 1的數相減得到的負數的絕對值顯然也不會超過2 ** 30 - 1
        //換句話說其結果對應的第31位一定是0, 那么再和32個位全部是1的2 ** 32次方相加, 得到的結果的第31位一定是1
        //所以再讓borrow右移30位、並和1進行與運算。如果結果為1, 證明相減為負數, 確實像下一個元素借了1, 因此下一次循環的會減去1
        //如果borrow為0, 那么就證明a->ob_digit[i] - b->ob_digit[i]得到的結果為正,根本不需要借位, 所以下一次循環等於減了一個0
    }
    
    //如果size_a和size_b一樣, 那么這里的for循環是不會滿足條件的, 但不一樣的話, 肯定會走這里
    for (; i < size_a; ++i) {
        //我們看到這里的邏輯和之前分析x_add是類似的
        borrow = a->ob_digit[i] - borrow;
        z->ob_digit[i] = borrow & PyLong_MASK;
        borrow >>= PyLong_SHIFT;
        borrow &= 1; 
    }
    //只不過由於不會產生進位, 因此不需要對borrow再做額外判斷, x_add中最后還要判斷carry有沒有進位
    assert(borrow == 0);
    if (sign < 0) {
        //如果sign < 0, 那么證明是負數
        Py_SIZE(z) = -Py_SIZE(z);
    }
    //最后同樣從后往前將z -> ob_digit中為0的元素刪掉, 直到遇見一個不為0的元素, 比如: 10000 - 9999, 雖然位數多, 但是結果是1
    //而z -> ob_digit在申請空間的時候只是根據長度申請的, 所以最后還需要這樣的一次判斷
    return long_normalize(z);
}

所以Python整數在底層的設計確實很精妙,尤其是在減法的時候,強烈建議多看幾遍回味一下。

整數的相減

整數的相減調用的是long_sub函數,顯然long_sub和long_add的思路都是一樣的,核心還是在x_add和x_sub上面,所以long_sub就沒有什么可細說的了。

static PyObject *
long_sub(PyLongObject *a, PyLongObject *b)
{	
    //z指向a和b相加之后的PyLongObject
    PyLongObject *z;
    //判斷a和b是否均指向PyLongObject
    CHECK_BINOP(a, b);
	
    //這里依舊是快分支
    if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
        //直接相減,然后轉成PyLongObject返回其指針
        return PyLong_FromLong(MEDIUM_VALUE(a) - MEDIUM_VALUE(b));
    }
    //a小於0
    if (Py_SIZE(a) < 0) {
        //a小於0,b小於0
        if (Py_SIZE(b) < 0)
            //調用絕對值減法, 因為兩者符號一樣
            z = x_sub(a, b);
        else
            //此時兩者符號不一樣,那么相加起到的是相加的效果
            z = x_add(a, b);
        if (z != NULL) {
            //但是x_add和x_sub運算的是絕對值, x_sub中考慮的sign是基於絕對值而言的
            //比如:x_sub接收的a和b的ob_size分別是-5和-3, 那么得到的結果肯定是正的, 因為會用絕對值大的減去絕對值小的
            //而顯然這里的結果應該是負數, 所以還要乘上-1
            
            //如果x_sub接收的a和b的ob_size分別是-3和-5, 由於還是用絕對值大的減去絕對值小的,所以會交換、從而變號,得到的結果是負的
            //而顯然這里的結果應該是正數, 所以也要乘上-1
            
            //至於x_add就更不用說了, 當a為負、b為正的時候, a - b,就等於a和b的絕對值相加乘上-1
            assert(Py_SIZE(z) == 0 || Py_REFCNT(z) == 1);
            Py_SIZE(z) = -(Py_SIZE(z));
        }
    }
    else {
        //a大於0, b小於0, 所以a - b等於a和b的絕對值相加
        if (Py_SIZE(b) < 0)
            z = x_add(a, b);
        else
            //a大於0, b大於0, 所以直接絕對值相減即可
            //而正數等於其絕對值, 所以x_sub里面考慮的符號就是真正的結果的符號
            //如果是上面的負數, 那么還要乘上-1
            z = x_sub(a, b);
    }
    //返回結果
    return (PyObject *)z;
}

所以關於什么時候調用x_add、什么時候調用x_sub,我們總結一下,總之核心就在於它們都是對絕對值進行運算的,掌握好這一點就不難了:

a + b

  • 如果a是正、b是正,調用x_add(a, b),直接對絕對值相加返回結果
  • 如果a是負、b是負,調用x_add(a, b),但相加的是絕對值,所以long_add中在接收到結果之后還要對ob_size乘上-1
  • 如果a是正、b是負,調用x_sub(a, b),此時等價於a的絕對值減去b的絕對值。並且x_sub是使用絕對值大的減去絕對值小的,如果a的絕對值大,那么顯然正常;如果a的絕對值小,x_sub中會交換,但同時也會自動變號,因此結果也是正常的。舉個普通減法的例子:5 + -3, 那么在x_sub中就是5 - 3; 如果是3 + -5, 那么在x_sub中就是-(5 - 3), 因為發生了交換。但不管那種情況,符號都是一樣的
  • 如果a是負、b是正,調用x_sub(b, a),此時等價於b的絕對值減去a的絕對值。所以這個和上面a是正、b是負是等價的。

所以符號相同,會調用x_add、符號不同會調用x_sub。

a - b

  • 如果a是正、b是負,調用x_add(a, b)直接對a和b的絕對值相加即可
  • 如果a是正、b是正,調用x_sub(a, b)直接對a和b的絕對值相減即可,會根據絕對值自動處理符號,而a、b為正,所以針對絕對值處理的符號,也是a - b的符號
  • 如果a是負、b是正,調用x_add(a, b)對絕對值進行相加, 但是結果顯然為負,因此在long_sub中還要對結果的ob_size成員乘上-1
  • 如果a是負、b是負,調用x_sub(a, b)對絕對值進行相減, 會根據絕對值自動處理符號, 但是在為負的情況下絕對值越大,其值反而越小, 因此針對絕對值處理的符號,和a - b的符號是相反的。所以最終在long_sub中,也要對結果的ob_size成員乘上-1。舉個普通減法的例子:-5 - -3, 那么在x_sub中就類似於5 - 3; 如果是-3 - -5, 那么在x_sub中就類似於-(5 - 3), 因為發生了交換。但不管那種情況得到的值的正負號都是相反的,所以要再乘上-1

所以符號相同,會調用x_sub、符號不同會調用x_add。

所以可以仔細回味一下Python中整數的設計思想,以及運算方式。為什么只使用digit的30個位, 以及在相加、相減的時候是怎么做的。

當然還有乘法和除法,乘法Python內部采用的是效率更高的karatsuba算法,相當來說比較復雜,有興趣可以自己查看一下。重點還是了解Python中的整數在底層是怎么存儲的,以及為什么要這么存儲。

小結

這一節我們介紹了整數的底層實現,並分析了Python中的整數為什么不會溢出,以及Python如何計算一個整數所占的字節。當然我們還說了小整數對象池,以及通過分析源碼中的long_add和long_sub來了解底層是如何對整數進行運算的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM