在最底層的硬體世界裡,電晶體只有開與關兩種狀態,我們習慣用1和0來表示。
這些就是 binary digits,簡稱 bits。
對於使用低階語言的系統程式設計師來說,了解bits和binary尤其重要。
然而,在運算歷史的大部分時間裡,程式設計的重點一直是找出越來越精巧的方法,來_抽象掉_這個二進位的現實。
在 Python(以及許多其他高階程式語言)中,我們使用int、float、string以及其他定義好的_型別_,一路涵蓋到音訊和視訊格式。
我們讓 Python 的內部機制負責(終究)把所有東西轉換成位元。
儘管如此,使用 位元運算子 和 位元運算 有時在速度和記憶體效率上能帶來顯著的優勢,即使是在像 Python 這樣的高階語言中也是如此。
毫不意外地,Python 與使用者互動時用的是十進位數字,但程式設計師可以覆寫這個預設行為。
事實上,Python 樂於接受binary、hexadecimal或octal格式的int,也很樂意對它們進行數學運算。
如果想了解更詳細的內容,可以複習 binary-octal-hexadecimal 這個概念。
二進位數字以0b開頭輸入,就像十六進位可以用0x(hex numbers 是簡潔表示 4 個位元為一組的方式),八進位數字則可以用oct。
要把整數轉成二進位字串有好幾種方法,差別在於是否包含0b前置符號,以及是否支援在左側補零:
# Binary entry.
>>> 0b10111
23
# Converting an int display to binary string, with prefix.
>>> bin(23)
'0b10111'
>>> number = 23
# Binary without prefix, padded to 8 digits.
>>> format(number, '08b')
'00010111'
# Same format, but using an f-string.
>>> f"{number} in decimal is {number:08b} in binary and {number:x} in hex"
'23 in decimal is 00010111 in binary and 17 in hex'
Bitwise Logic在 bools 這個概念中,我們討論過搭配布林值(True和False)使用的_邏輯運算子_and、or和not。
處理位元時,也適用同樣的邏輯規則。
不過,邏輯運算子&(and)、|(or)、~(not)和^(XOR)的位元版本,會套用到二進位表示法中的每個_位元_,把1當成True(「開」),把0當成False(「關」)。
用位元&舉個例子,或許能讓這件事更清楚:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
只有在_兩個_輸入數字中都為1的位置,輸出的該位置才會設為1。
位元&常被用來從一組壓縮過的True/False值中取出單一位元,例如應用程式中可由使用者設定的選項。
這麼一來,個別位元的值就能控制程式邏輯:
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
對位元|(or)來說,只要_其中一個_輸入在該位置是1,輸出的該位置就會設為1:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
至於^運算子(位元的 exclusive or,也就是 xor),若該位元出現在_其中一個輸入中,但不是兩個都有,輸出的該位置就會設為1。
這個符號你可能在 sets 這個概念中看過,在那裡它用於set的_對稱差集,這和 xor 套用於集合 是同一回事。
如果 xor ^ 看起來很陌生,要知道它可是密碼學中最常見的運算。
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
最後是~運算子(也就是波浪號這個字元),它是位元的not,只接受一個輸入,並_把所有位元反轉_,這結果可能和你想的不一樣!
表示法中的每個1都會變成0,反之亦然。
詳情請見下一節。
在十進位表示法中,我們用位在數字左邊的+或-號來區分正數和負數。
但在二進位的層級使用這些符號,對數位運算來說並不有效率,還帶來了+0和-0並不相同的問題。
所有現代電腦不用-和+,而是採用 two's complement 來表示負數,一路到矽晶片層級都是如此。
這表示所有位元都會反轉,而且如果最左邊的位元(也稱為「最高有效位元」,即 most significant bit,簡稱 MSB)是1,這個數字就會被_解讀為負數_。
正數的 MSB 是0。
這種表示法的優點是零只有一種版本,程式設計師不必去處理-0和+0。
這種表示正負數的方式,為 Python 帶來了一個麻煩:核心語言內部並沒有像int32或int64這樣的有限整數概念。
在「現代」Python 中,int的大小沒有限制(只受限於硬體容量),而負數或被位元反轉的數字,左邊有(理論上)無限多個1,就像正數有無限多個0一樣。
這使得我們很難給出一個實用的bitwise not範例:
>>> x = 0b01100110
>>> format(x, '08b')
'01100110'
# This is a negative binary (not twos-complement display).
>>> format(~x, '08b')
'-1100111'
# Decimal representation.
>>> x
102
# Using the Bitwise not, with an unintuitive result.
>>> ~x
-103
這不是我們在有固定大小整數的語言中會看到的0b10011001。
~運算子只有在_無號_的位元組或整數型別,或固定大小的整數型別上,才會如預期運作。
這些數值型別在 NumPy、pandas 和 sympy 這類第三方套件中都有支援,但核心 Python 並不支援。
實務上,Python 程式設計師經常用&、|、^以及下面介紹的移位運算子,而且通常只用正數。
對負數做位元運算則少見得多。
有一種技巧是把 2**32 (or 1 << 32) 加到負數值上,讓int變成無號,但這很難管理。
另一種策略是使用 ctypes 模組,採用 C 風格的整數型別,但這也同樣笨重。
Shift operators左移運算子x << y會把x中的所有位元向左移動y個位置,並在新的空位補上零。
請注意,這在算術上等同於把數字乘以(2**y)。
右移運算子x >> y則相反。
這在算術上等同於整數除法x // (2**y)。
請記得上一節關於負數的內容,以及在 Python 中對負數做移位時的陷阱。
>>> x = 8
>>> format(x, '08b')
'00001000'
# A left bit shift.
>>> x << 2
32
>>> format(x << 2, '08b')
'00100000'
# A right bit shift.
>>> format(x >> 2, '08b')
'00000010'