Python處理二進位制結構化資料時的技術細節
阿新 • • 發佈:2019-01-07
Find an English version here
轉載請保留原文地址 http://blog.csdn.net/ir0nf1st/article/details/69583752
<0x00> 前言
處理二進位制檔案或者從網路接收位元組流時,位元組流中的結構化資料可能存在二進位制有符號數。雖然開發者根據位元組流協議可以先驗的知道有符號數的位元組序、字長、符號位等資訊,但在使用Python進行型別轉換時缺少將這些資訊顯式傳遞給Python直譯器的手段。本文介紹了兩種在Python開發中處理二進位制有符號數的方法。<0x01> Python如何處理數字型別
很多程式語言在處理有符號數時將數字的最高位作為符號位,但Python的數字型別實現有所不同。以下是CPython中對長整形的定義:(以下皆為Python 2.7版本) include/longobject.h/* Long (arbitrary precision) integer object interface */
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
include/longintrepr.h
/* Long integer representation. The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) Negative numbers are represented with ob_size < 0; zero is represented by ob_size == 0. In a normalized number, ob_digit[abs(ob_size)-1] (the most significant digit) is never zero. Also, in all cases, for all valid i, 0 <= ob_digit[i] <= MASK. The allocation function takes care of allocating extra memory so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available. CAUTION: Generic code manipulating subtypes of PyVarObject has to aware that longs abuse ob_size's sign bit. */ struct _longobject { PyObject_VAR_HEAD digit ob_digit[1]; };
include/object.h
從以上程式碼以及註釋中我們知道,Python中的長整形,即_longobject,其符號位是通過PyObject_VAR_HEAD中的ob_size來表示,而ob_digit只儲存著長整形的絕對值。 另一方面,當我們初始化一個長整形物件(即將一個數值賦值給物件)的時候,Python直譯器並不會把這個數值的最高位當作符號位而是將其當作有效位。 下面以數值-500為例,來看看使用該數值對Python物件賦值會發生什麼,簡便起見,這裡使用16bit字長。 我們知道,為了計算方便,負數在計算機中以其二進位制補碼形式儲存,我們首先將-500轉換為二進位制補碼:/* PyObject_VAR_HEAD defines the initial segment of all variable-size * container objects. These end with a declaration of an array with 1 * element, but enough space is malloc'ed so that the array actually * has room for ob_size elements. Note that ob_size is an element count, * not necessarily a byte count. */ #define PyObject_VAR_HEAD \ PyObject_HEAD \ Py_ssize_t ob_size; /* Number of items in variable part */
十進位制 | -500 |
十六進位制 | -0x01 F4 |
二進位制 | -0b 0000 0001 1111 0100 |
二進位制原碼(符號在最高位) | 0b 1000 0001 1111 0100 |
二進位制反碼(除符號位外,對原碼按位取反) | 0b 1111 1110 0000 1011 |
二進位制補碼(二進位制反碼加1) | 0b 1111 1110 0000 1100 |
二進位制補碼的十六進位制表示 | 0xFE 0C |
>>> stream = '\xFE\x0C'
>>> number = (ord(stream[0]) << 8) + ord(stream[1])
>>> '0x{:0X}'.format(number)
'0xFE0C'
>>> print number
65036
>>>
這顯然不是我們想要的結果,為了讓Python在以有符號數對物件進行初始化時正確工作,上面的程式碼需要修改。
<0x02> 使用負號傳遞數值的符號資訊
現在我們知道Python並不從數字的符號位讀取符號資訊,為了讓Python正確的處理該數值,開發者必須顯示的通知Python直譯器:這是一個負數。開發者可以藉助負號(-),即negative操作符來完成這個任務。 先看一下negtive操作符在CPython中的實現: Objects/longobject.cstatic PyObject *
long_neg(PyLongObject *v)
{
PyLongObject *z;
if (v->ob_size == 0 && PyLong_CheckExact(v)) {
/* -0 == 0 */
Py_INCREF(v);
return (PyObject *) v;
}
z = (PyLongObject *)_PyLong_Copy(v);
if (z != NULL)
z->ob_size = -(v->ob_size);
return (PyObject *)z;
}
可見negtive操作符只對_longobject的ob_size域(即Python記載的符號位)進行了negtive操作,而ob_digit域不變。 我們還知道,如果數值value為負數,則有value = - abs(value)。當我們將負數的絕對值和negtive操作符一起傳遞給Python直譯器,Python直譯器就能正確的處理該數值。 對於本例中的二進位制串0xFE 0x0C,我們已經先驗的知道了該串的位元組序,字長資訊,所以其符號位在最高位即bit15。因符號位為1,則該數值為負數。下一步是對該數值求絕對值,由於數值是二進位制補碼形式儲存,則其絕對值為二進位制補碼減去1(即其二進位制反碼)再按位取反。 程式碼如下:
>>> number = 0xFE0C
>>> if (number & 0x8000) != 0:
... number = -((number - 1) ^ 0xFFFF)
...
>>> print number
-500
>>>
細心的讀者可能會注意到上面程式碼中按位取反操作並沒有使用~(即invert操作符),而是使用的和0xFFFF異或來實現。 如果改為invert操作符會如何呢?
>>> number = 0xFE0C
>>> if (number & 0x8000) != 0:
... number = -(~(number - 1))
...
>>> print number
65036
>>>
還是讓我們來看一下CPython對invert操作符的實現:
Objects/longobject.cstatic PyObject *
long_invert(PyLongObject *v)
{
/* Implement ~x as -(x+1) */
PyLongObject *x;
PyLongObject *w;
w = (PyLongObject *)PyLong_FromLong(1L);
if (w == NULL)
return NULL;
x = (PyLongObject *) long_add(v, w);
Py_DECREF(w);
if (x == NULL)
return NULL;
Py_SIZE(x) = -(Py_SIZE(x));
return (PyObject *)x;
}
正如程式碼中的註釋說明,Python的invert操作符的實現並不是真的按位取反,而是對數值加1後的negtive操作。
<0x03> 一個更通用的庫
struct是一個對二進位制結構化資料解包與打包的庫,可以讓你使用對開發者更友好的方式來傳遞位元組序、字長、符號位等資訊,同時也有著足夠的錯誤報告機制,讓你的開發更高效。
>>> import struct
>>> stream = '\xFE\x0C'
>>> number, = struct.unpack('>h', stream)
>>> print number
-500
>>>
'>h'是struct支援的格式化串,‘>'表明位元組流為大位元組序,‘h'表明位元組流包含signed short。關於struct模組更多的說明可參見這個連結。