問題是這個樣子的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
➜ cpython git:(84471935ed) ✗ python2 Python 2.7.15 (default, Jun 27 2018, 13:05:28) [GCC 8.1.1 20180531] on linux2 Type “help”, “copyright”, “credits” or “license” for more information. >>> (–1) ** .5 Traceback (most recent call last): File “<stdin>”, line 1, in <module> ValueError: negative number cannot be raised to a fractional power >>> ➜ cpython git:(84471935ed) ✗ python3 Python 3.7.0 (default, Jul 15 2018, 10:44:58) [GCC 8.1.1 20180531] on linux Type “help”, “copyright”, “credits” or “license” for more information. >>> (–1) ** .5 (6.123233995736766e–17+1j) >>> |
為什麼會有 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):
1 2 3 |
$ git pull https://github.com/python/cpython $ ./configure —with–pydebug $ make –j8 |
完成後寫個簡單的 script 放著:
1 2 |
a = –1 b = (a) ** .5 |
我們可以透過 dis
查看一下 OPCODE:
1 2 3 4 5 6 7 8 9 10 |
$ ./python –m dis test.py 1 0 LOAD_CONST 0 (–1) 2 STORE_NAME 0 (a) 2 4 LOAD_NAME 0 (a) 6 LOAD_CONST 1 (0.5) 8 BINARY_POWER 10 STORE_NAME 1 (b) 12 LOAD_CONST 2 (None) 14 RETURN_VALUE |
這段只是讓我們知道說,執行的 OPCODE 是在 BINARY_POWER 這邊,不過我們最主要還是要看 int_pow / long_pow 這邊。
gdb 追蹤 Python 3 code
1 2 3 4 5 6 7 8 9 10 11 |
$ gdb —args ./python test.py (gdb) b long_pow Breakpoint 1 at 0x8ca28: file Objects/longobject.c, line 4119. (gdb) r Starting program: /home/louie/dev/cpython/python test.py [Thread debugging using libthread_db enabled] Using host libthread_db library “/usr/lib/libthread_db.so.1”. Breakpoint 1, long_pow (v=0x5555558b0e00 <small_ints+192>, w=0x7ffff78a6478, x=0x555555858500 <_Py_NoneStruct>) at Objects/longobject.c:4119 4119 { (gdb) |
來到這邊,就是實際進行 pow 運算的 function,我們可以透過 Ctrl+x a
叫出 tui,方便觀察程式碼的運行狀況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
┌──Objects/longobject.c─────────────────────────────────────────────────────┐ │4114 } │ │4115 │ │4116 /* pow(v, w, x) */ │ │4117 static PyObject * │ │4118 long_pow(PyObject *v, PyObject *w, PyObject *x) │ B+>│4119 { │ │4120 PyLongObject *a, *b, *c; /* a,b,c = v,w,x */ │ │4121 int negativeOutput = 0; /* if x<0 return negative output */ │ │4122 │ │4123 PyLongObject *z = NULL; /* accumulated result */ │ │4124 Py_ssize_t i, j, k; /* counters */ │ │4125 PyLongObject *temp = NULL; │ │4126 │ └───────────────────────────────────────────────────────────────────────────┘ multi–thre Thread 0x7ffff7c110 In: long_pow L4119 PC: 0x5555555e0a28 (gdb) |
這樣還不能滿足,我們應該要退後一層回到看是哪個 abstract function 呼叫 long_pow 的,用 bt
來找出 stack 中呼叫 long_pow 的 function,可以發現到是 PyNumber_Power
這個 abstract function 在處理。把 gdb 關掉重開,斷點設定在 PyNumber_Power
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
┌──Objects/abstract.c───────────────────────────────────────────────────────┐ │1022 return binary_op(v, w, NB_SLOT(nb_remainder), “%”); │ │1023 } │ │1024 │ │1025 PyObject * │ │1026 PyNumber_Power(PyObject *v, PyObject *w, PyObject *z) │ B+ │1027 { │ >│1028 return ternary_op(v, w, z, NB_SLOT(nb_power), “** or pow()”); │ │1029 } │ │1030 │ │1031 /* Binary in-place operators */ │ │1032 │ │1033 /* The in–place operators are defined to fall back to the ‘normal’,│ │1034 non in–place operations, if the in–place methods are not in plac│ └───────────────────────────────────────────────────────────────────────────┘ multi–thre Thread 0x7ffff7c110 In: PyNumber_Power L1028 PC: 0x555555734531 (gdb) n (gdb) s |
透過 s step 進去 ternary_op 之中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
┌──Objects/abstract.c───────────────────────────────────────────────┐ │875 ternaryfunc slotz = NULL; │ │876 │ │877 mv = v->ob_type->tp_as_number; │ │878 mw = w->ob_type->tp_as_number; │ │879 if (mv != NULL) │ │880 slotv = NB_TERNOP(mv, op_slot); │ │881 if (w->ob_type != v->ob_type && │ │882 mw != NULL) { │ │883 slotw = NB_TERNOP(mw, op_slot); │ │884 if (slotw == slotv) │ │885 slotw = NULL; │ │886 } │ >│887 if (slotv) { │ │888 if (slotw && PyType_IsSubtype(w->ob_type, v->ob_typ│ └───────────────────────────────────────────────────────────────────┘ Thread 0x7ffff7c110 In: ternary_op L887 PC: 0x555555733114 (gdb) |
可以看到 slotv 是 v object (-1) 的相對應的 function, slotw 則是 w object (0.5) 相對應的 function,可以各自用 print 查看:
1 2 3 4 5 |
(gdb) p slotv $1 = (ternaryfunc) 0x5555555e0a28 <long_pow> (gdb) p slotw $2 = (ternaryfunc) 0x5555555ccaf1 <float_pow> (gdb) |
接著下面的開始針對 slotv 以及 slotw 做運算,最主要是這個部份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if (slotv) { ... x = slotv(v, w, z); if (x != Py_NotImplemented) return x; ... } if (slotw) { ... x = slotw(v, w, z); if (x != Py_NotImplemented) return x; .... } |
首先使用 slotv 如果有值則回傳,如果沒有的話繼續往 slotw 嘗試。以我們的案例而言,slotv 會回傳 Py_NotImplemented,因為 long_pow 只有實做兩個型態都為 long 的時候才能夠運算的 power:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
┌──Objects/longobject.c─────────────────────────────────────────────┐ │4129 */ │ │4130 PyLongObject *table[32] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,│ │4131 0,0,0,0,0,0,0,0,0,0,0,0,0,0,│ │4132 │ │4133 /* a, b, c = v, w, x */ │ >│4134 CHECK_BINOP(v, w); │ │4135 a = (PyLongObject*)v; Py_INCREF(a); │ │4136 b = (PyLongObject*)w; Py_INCREF(b); │ │4137 if (PyLong_Check(x)) { │ │4138 c = (PyLongObject *)x; │ │4139 Py_INCREF(x); │ │4140 } │ │4141 else if (x == Py_None) │ └───────────────────────────────────────────────────────────────────┘ Thread 0x7ffff7c110 In: long_pow L4134 PC: 0x5555555e0a65 |
CHECK_BINOP 是這樣定義的:
1 2 3 4 5 |
#define CHECK_BINOP(v,w) \ do { \ if (!PyLong_Check(v) || !PyLong_Check(w)) \ Py_RETURN_NOTIMPLEMENTED; \ } while(0) |
而 v 以及 w 的型態不相同 (一個是 longobject 一個是 floatobject),因此會直接回傳Py_NotImplemented。
接著會嘗試 slotw 的部份 (所以,其實使用的是後面那個 0.5 的 __pow__ slot 來運算的),主要做的東西就是轉型為 float 然後根據不同的 v 以及 w 值做運算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... │695 CONVERT_TO_DOUBLE(v, iv); │ │696 CONVERT_TO_DOUBLE(w, iw); │ ... │749 if (iv < 0.0) { │ │750 /* Whether this is an error is a mess, and bumps into libm │ │751 * bugs so we have to figure it out ourselves. │ │752 */ │ │753 if (iw != floor(iw)) { │ │754 /* Negative numbers raised to fractional powers │ │755 * become complex. │ │756 */ │ >│757 return PyComplex_Type.tp_as_number->nb_power(v, w, z); │ │758 } │ |
結果出來了,在 Python 3 當中,(-1) ** .5 最後會使用 PyComplex_Type 的 power 來做運算,因此結果被轉型為 complex 了。
gdb 追蹤 Python 2 code
那如果是 Python 2 呢?一樣先編譯好之後重新斷點 PyNumber_Power:
1 2 3 4 5 6 7 |
$ git checkout v2.7.14 $ ./configure —with–pydebug $ make –j8 $ gdb —args ./python test.py (gdb) b PyNumber_Power (gdb) r (gdb) |
根據剛剛 Python 3 的經驗,查看 slotv 以及 slotw 的結果:
1 2 3 4 5 |
(gdb) p slotv $4 = (ternaryfunc) 0x5555555b9df3 <int_pow> (gdb) p slotw $5 = (ternaryfunc) 0x5555555b45df <float_pow> (gdb) |
進入 slotv 後,執行到 CONVERT_TO_LONG 就被 return 了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define CONVERT_TO_LONG(obj, lng) \ if (PyInt_Check(obj)) { \ lng = PyInt_AS_LONG(obj); \ } \ else { \ Py_INCREF(Py_NotImplemented); \ return Py_NotImplemented; \ } ... │725 static PyObject * │ │726 int_pow(PyIntObject *v, PyIntObject *w, PyIntObject *z) │ │727 { │ │728 register long iv, iw, iz=0, ix, temp, prev; │ │729 CONVERT_TO_LONG(v, iv); │ > │730 CONVERT_TO_LONG(w, iw); │ |
在 int_pow 一樣只有支援 long ** long 而已,於是改用 slotw 運算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
│818 static PyObject * │ │819 float_pow(PyObject *v, PyObject *w, PyObject *z) │ │820 { │ │821 double iv, iw, ix; │ │822 int negate_result = 0; │ ... │884 if (iv < 0.0) { │ │885 /* Whether this is an error is a mess, and bumps into libm │ │886 * bugs so we have to figure it out ourselves. │ │887 */ │ │888 if (iw != floor(iw)) { │ │889 PyErr_SetString(PyExc_ValueError, “negative number “ │ │890 “cannot be raised to a fractional power”); │ >│891 return NULL; │ │892 } │ |
可以看到在 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 表示。
Leave a Reply