Python 原始碼理解: '+=' 和 'xx = xx + xx' 的區別

發表於2017-08-07

前菜

在我們使用Python的過程, 很多時候會用到+運算, 例如:

不光在加法中使用, 在字串的拼接也同樣發揮這重要的作用, 例如:

同樣的, 在列表中也能使用, 例如:

為什麼上面不同的物件執行同一個+會有不同的效果呢? 這就涉及到+的過載, 然而這不是本文要討論的重點, 上面的只是前菜而已~~~

正文

先看一個例子:

這段程式碼的用途很明確, 就是一個簡單的數字相加, 但是這樣似乎很繁瑣, 一點都Pythonic, 於是就有了下面的程式碼:

哈, 這樣就很Pythonic了! 但是這種用法真的就是這麼好麼? 不一定. 看例子:

看起來結果都一樣嘛~, 但是真的一樣嗎? 我們改下程式碼再看下:

看到結果了嗎? 雖然結果一樣, 但是通過id的值表示, 運算前後, 第一種方法物件是不同的了, 而第二種還是同一個物件! 為什麼會這樣?

結果分析

先來看看位元組碼:

在上訴的位元組碼, 我們著重需要看的是兩個: BINARY_ADDINPLACE_ADD!

很明顯:
l = l + [3, 4, 5]    這種背後就是BINARY_ADD
l += [3, 4, 5]     這種背後就是INPLACE_ADD

深入理解

雖然兩個單詞差很遠, 但其實兩個的作用是很類似的, 最起碼前面一部分是, 為什麼這樣說, 請看原始碼:

從上面可以看出, 不管是BINARY_ADD 還是INPLACE_ADD, 他們都會有如下相同的操作:

因為兩者的行為真的很類似, 所以在這著重講INPLACE_ADD,BINARY_ADD感興趣的童鞋可以在原始碼檔案: abstract.c, 搜尋: PyNumber_Add.實際上也就少了對列表之類物件的操作而已.

那我們接著繼續, 先貼個原始碼:

INPLACE_ADD本質上是對應著abstract.c檔案裡面的PyNumber_InPlaceAdd函式, 在這個函式中, 首先呼叫binary_iop1函式, 然後進而又呼叫了裡面的binary_op1函式, 這兩個函式很大一個篇幅, 都是針對ob_type->tp_as_number, 而我們目前是list, 所以他們的大部分操作, 都和我們的無關. 正因為無關, 所以這兩函式呼叫最後, 直接返回Py_NotImplemented, 而這個是用來幹嘛, 這個有大作用, 是列表相加的核心所在!

因為binary_iop1的呼叫結果是Py_NotImplemented, 所以下面的判斷成立, 開始尋找物件(也就是演示程式碼中l物件)ob_type->tp_as_sequence屬性.

因為我們的物件是l(列表), 所以我們需要去PyList_type需找真相:

可以看出, 其實也就是直接取list_as_sequence, 而這個是什麼呢? 其實是一個結構體, 裡面存放了列表的部分功能函式.

接下來就是一個判斷, 判斷我們們這個l物件是否有Py_TPFLAGS_HAVE_INPLACEOPS這個特性, 很明顯是有的, 所以就呼叫上步取到的結構體中的sq_inplace_concat函式, 那接下來呢? 肯定就是看看這個函式是幹嘛的:

終於找到關鍵了, 原來最後就是呼叫這個listextend函式, 這個和我們python層面的列表的extend方法很類似, 在這不細講了!

PyNumber_InPlaceAdd的執行呼叫過程, 簡單整理下來就是:

所以在上面的結果, 第二種程式碼: l += [3,4,5], 我們看到的id值並沒有改變, 就是因為+=通過sq_inplace_concat呼叫了列表的listextend函式, 然後導致新列表以追加的方式去處理.

結論

現在我們大概明白了+=實際上是幹嘛了: 它應該能算是一個加強版的+, 因為它比+多了一個寫回本身的功能.不過是否能夠寫回本身, 還是得看物件自身是否支援, 也就是說是否具備Py_NotImplemented標識, 是否支援sq_inplace_concat, 如果具備, 才能實現, 否則, 也就是和 + 效果一樣而已.

相關文章