Why python 2: negative number cannot be raised to a fractional power, but work on Python 3

問題是這個樣子的

為什麼會有 Python 2 不能運算 (-1) ** .5 而 Python 3 可以的狀況?直覺想就是 Python 2 的 integer __pow__ 並沒有支援 pow 運算轉型為 Complex,而在 Python 3 中會轉行為 Complex。但是要怎麼驗證這個行為?

編譯 CPython 並且使用 gdb 追蹤

根據以前接觸 CPython 的狀況,總之找出 object slot function 後就能夠設定斷點追蹤,以這個例子而言,在 Python 2 是 Object/intobject.c:int_pow 承接,Python 3 則是 Object/longobject.c:long_pow 承接。(至於 Python 2 是 intobject, Python 3 卻改成 longobject 的原因,這又是另一個故事了)。

找出需要斷點的地方後,先把 CPython pull 下來編譯好,master branch的是最新版本的 Python (目前為 3.8):

完成後寫個簡單的 script 放著:

我們可以透過 dis 查看一下 OPCODE:

這段只是讓我們知道說,執行的 OPCODE 是在 BINARY_POWER 這邊,不過我們最主要還是要看 int_pow / long_pow 這邊。

gdb 追蹤 Python 3 code

來到這邊,就是實際進行 pow 運算的 function,我們可以透過 Ctrl+x a 叫出 tui,方便觀察程式碼的運行狀況。

這樣還不能滿足,我們應該要退後一層回到看是哪個 abstract function 呼叫 long_pow 的,用 bt 來找出 stack 中呼叫 long_pow 的 function,可以發現到是 PyNumber_Power 這個 abstract function 在處理。把 gdb 關掉重開,斷點設定在 PyNumber_Power

透過 s step 進去 ternary_op 之中:

可以看到 slotv 是 v object (-1) 的相對應的 function, slotw 則是 w object (0.5) 相對應的 function,可以各自用 print 查看:

接著下面的開始針對 slotv 以及 slotw 做運算,最主要是這個部份:

首先使用 slotv 如果有值則回傳,如果沒有的話繼續往 slotw 嘗試。以我們的案例而言,slotv 會回傳 Py_NotImplemented,因為 long_pow 只有實做兩個型態都為 long 的時候才能夠運算的 power:

CHECK_BINOP 是這樣定義的:

而 v 以及 w 的型態不相同 (一個是 longobject 一個是 floatobject),因此會直接回傳Py_NotImplemented。

接著會嘗試 slotw 的部份 (所以,其實使用的是後面那個 0.5 的 __pow__ slot 來運算的),主要做的東西就是轉型為 float 然後根據不同的 v 以及 w 值做運算:

結果出來了,在 Python 3 當中,(-1) ** .5 最後會使用 PyComplex_Type 的 power 來做運算,因此結果被轉型為 complex 了。

gdb 追蹤 Python 2 code

那如果是 Python 2 呢?一樣先編譯好之後重新斷點 PyNumber_Power:

根據剛剛 Python 3 的經驗,查看 slotv 以及 slotw 的結果:

進入 slotv 後,執行到 CONVERT_TO_LONG 就被 return 了:

在 int_pow 一樣只有支援 long ** long 而已,於是改用 slotw 運算:

可以看到在 iv < 0.0 的情況,如果  iw != floor(iw),就會拋出 ValueError 說明 negative number cannot be raised to a fractional power。這也說明了,Python 2 在這邊不會主動的做轉型,在 float pow 上不支援 iv < 0.0 的情況做 fractional power。

總結

  • Python 2 / 3 對於 int_pow / long_pow 都要求 v ^ w 的兩個運算子都是 int / long type,如果不是的話,會改用另一個 power function 運算。以 -1 ** 0.5 的例子,會使用 0.5 的 __pow__ slot (float_pow)
  • Python 2 的 float_pow 對於 iv < 0.0 而 iw 為 fractional 的狀況,不會自動轉型為 complex,而是拋出 ValueError
  • Python 3 的 float_pow 則會改以 complex 的 __pow__ slot 運算,這讓運算結果會以 complex type 表示。



如果你覺得這篇文章不錯,歡迎打賞 IOTA:RFHEIVXVIZWJFXTZORZZRCMHZF9PSGFUFWAAKXTFNZE9JQUY9HFQREJYYSPSXDRLECKXCAQQDOMSMYJYDKPCKWXBKD

或是點選下方圖片贊助我一杯咖啡:

Leave a reply:

Your email address will not be published.

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料