トラック
/
Python
Python
/
シラバス
/
ビット演算子
ビッ

ビット演算子 の Python

{other: "%{count}個の演習"}

ビット演算子について

ハードウェアのレベルまで下りていくと、トランジスターはオンかオフのどちらかしかありません。この2つの状態を、伝統的に1と0で表します。これがbinary digitsで、bitsと略されます。bitsとbinaryについての知識は、低水準言語で働くシステムプログラマーにとって特に重要です。

しかし、コンピューティングの歴史のほとんどにおいて、プログラミングの優先事項は、この2進法の現実を_抽象化して隠す_、より洗練された方法を見つけることでした。Python(および他にも多くの高水準プログラミング言語)では、int、float、stringなどの定義された_型_を扱い、音声や動画のフォーマットにまで及びます。すべてを(最終的には)ビットに変換する処理は、Pythonの内部に任せています。

とはいえ、ビット演算子とビット演算を使うことで、Pythonのような高水準言語であっても、速度とメモリ効率の面で大きな利点が得られることがあります。

2進数の入力と表示

当然のことながら、Pythonはユーザーと10進数でやり取りしますが、プログラマーはこの既定の動作を上書きできます。実際、Pythonはbinary、hexadecimal、octal形式のintをそのまま受け入れ、それらの間で数学的な演算も問題なく行えます。詳しくは、binary-octal-hexadecimalのコンセプトを参照してください。

2進数は0bという接頭辞を付けて入力します。同じように、16進数には0x(16進数は4ビットのまとまりを簡潔に表す方法です)を、8進数にはoctを使うことができます。

整数を2進数の文字列に変換する方法はいくつかあり、0b接頭辞を含むかどうか、また0で左詰めするかどうかが異なります。

# 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)は、2進表現の_各ビット_に対して適用され、1をTrue(「オン」)、0をFalse(「オフ」)として扱います。ビット単位の&を使った例を見ると、わかりやすいでしょう。

>>> x = 0b01100110
>>> y = 0b00101010

>>> format(x & y, '08b')
'00100010'

入力した2つの数値の_両方_で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'

ビット単位の排他的論理和(xor)を表す^演算子では、入力のどちらか一方にのみ1がある場合に1が立ちます。この記号は、setsのコンセプトで見覚えがあるかもしれません。そこではsetの_対称差_に使われ、これは集合に適用したxorと同じものです。xorの^が奇妙に思えるなら、これが暗号技術で最もよく使われる演算であることを知っておくとよいでしょう。

>>> x = 0b01100110
>>> y = 0b00101010

>>> format(x ^ y, '08b')
'01001100'

最後に、~演算子(チルダの文字)があります。これはビット単位のnotで、1つの入力を受け取り、_すべてのビットを反転_させます。期待した結果とは違うかもしれません。表現の中の1はそれぞれ0に変わり、その逆も同様です。詳しくは下のセクションを参照してください。

負の数と2進表現

10進表現では、数字の左に+または-の符号を付けて、正の数と負の数を区別します。しかし、この記号を2進のレベルで使うのはデジタル計算には効率が悪く、+0と-0が同じでないという問題も生じました。

現代のコンピューターは、-と+を使う代わりに、シリコンチップのレベルに至るまで、負の数の表現にtwo's complementを使っています。これは、すべてのビットを反転させ、左端のビット(「最上位ビット」、MSBとも呼ばれます)が1であれば、その数を_負として解釈する_という意味です。正の数では、MSBは0です。この表現には、ゼロの表し方が1つしかないという利点があり、プログラマーは-0と+0を管理する必要がありません。

この正負の数の表現方法は、Pythonにとってひとつの複雑さをもたらします。コア言語の内部には、int32やint64のような有限の整数という概念がないのです。「現代の」Pythonでは、intの大きさに制限はなく(ハードウェアの容量によってのみ制限されます)、負の数やビット反転した数は、正の数が無限に0を持つように、左側に(理論上)無限に1が並びます。

このため、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桁だけ左に移動させ、新しくできた隙間を0で埋めます。これは算術的には、数値を(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で編集 リンクは新しいウィンドウまたはタブで開きます