軌道
/
Python
Python
/
課程大綱
/
位元運算子
位元

位元運算子 在 Python

{other: "%{count} 個練習"}

關於 位元運算子

在最底層的硬體世界裡,電晶體只有開與關兩種狀態,我們習慣用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'
透過 GitHub 編輯 連結會在新視窗或分頁中開啟