はじめに
Pythonにおける論理積の記述方法にはandと&があります。また論理和にはorと|があります。
これらの記号はbool型の変数に対してはそれぞれ同一の挙動を示します。ではそれぞれ二通りの表記があるというだけで同じものなのでしょうか。結論から言えばこれらは似て非なるものです。
ブール演算 (boolean operation)
ブール演算子にはandやorが含まれます。この演算子はbooleanとは言うもののオペラントの型は任意です。結果の型もbool型である必要はありません。Python3.13.7の公式文書では
式 x and y は、まず x を評価します; x が偽なら x の値を返します; それ以外の場合には、 y を評価した結果値を返します。
式 x or y は、まず x を評価します; x が真なら x の値を返します; それ以外の場合には、 y を評価した結果値を返します。
—―Python 公式ドキュメント 6.11
と説明されています。ここからか分かるように、x and yやx or yで戻ってくる値はxの値やyの値そのものということになるのです。簡単に試験的なコードを書いて確かめてみましょう。
for i in range(3):
for j in range(3):
print(i, j, i and j, i or j)
結果は、
0 0 0 0
0 1 0 1
0 2 0 2
1 0 0 1
1 1 1 1
1 2 2 1
2 0 0 2
2 1 1 2
2 2 2 2
となりました。
andでは最初のオペラント(andやorなどのオペレーターを挟むxやyのこと)が0でないときは二つ目のオペラントの値を返しています。また、orでは最初のオペラントが0でないときはそのまま最初のオペラントの値を返していることがわかります。0でない値を持つことを真とすれば、これは通常のbool型の論理演算に対応付けられる結果です。
ブール演算の結果は必ずしもブール型ではない。
ビット単位演算の二項演算 (binary bitwise operation)
&や|にはビット単位演算の二項演算 (binary bitwise operation)という名前がついているようです。あまりメジャーではないように思いますが、排他的論理和を^で書くこともでき、これも二項演算の仲間です。
ブール演算と同様、こちらについても公式ドキュメントを読んでみましょう。
The & operator yields the bitwise AND of its arguments, which must be integers or one of them must be a custom object overriding
__and__()or__rand__()special methods.
The ^ operator yields the bitwise XOR (exclusive OR) of its arguments, which must be integers or one of them must be a custom object overriding__xor__()or__rxor__()special methods.
The | operator yields the bitwise (inclusive) OR of its arguments, which must be integers or one of them must be a custom object overriding__or__()or__ror__()special methods.
—―Python 公式ドキュメント 6.9
こちらについては日本語化されていませんでした。一文目は&の説明で、日本語に訳せば
& 演算子は引数のビット単位のANDを生成します。引数は整数であるか、あるいは少なくとも一方が
__and__()または__rand__()特殊メソッドをオーバーライドするカスタムオブジェクトでなければなりません。
となります。他の|や^についてもおおよそ同じで、これらの記号はビット演算であるということが重要ポイントです。
ブール演算で実験に使ったコードをビット単位演算の二項演算に置き換えて試してみましょう。
for i in range(3):
for j in range(3):
print(i, j, i & j, i | j, i ^ j)
出力は、
0 0 0 0 0
0 1 0 1 1
0 2 0 2 2
1 0 0 1 1
1 1 1 1 0
1 2 0 3 3
2 0 0 2 2
2 1 0 3 3
2 2 2 2 0
となります。
andやorとは全く異なる挙動を示していることが分かります。ビット単位の演算とはオペラントをビット列(二進法の表記)に変換して演算を行うという意味です。例えば2は二進法では10なので、1とのビット単位での論理積は0、論理和は11(10進法では3)となります。
ビットの計算に慣れていない方にとってはこの仕組みはもっと大きな数の2進法にしたほうが分かりやすいかもしれません。例えば19は二進法で10011、5は二進法で101なので19 & 5は10011と101の論理積、つまり10001(十進法で17)です。
このことを利用すれば偶奇の判定にも応用できます。
x = 1023
if x & 1:
print("Odd")
else:
print("Even")
このコードの出力は、
Even
となり、xの偶奇を正しく判定できます。
なぜこれで偶奇の判定ができるのでしょうか。理由はシンプルで、1とのビット単位の論理積を調べることは調べたい数の二進法での1の位を調べることと同義だからです。偶数を二進法にしたとき最小の1の位は必ず0となり、奇数は1となります。
しかしx % 2 == 0で判定するほうがはるかに読みやすいので積極的に書きたいコードではないというのが個人的な見解です。
補足——特殊メソッドについて——
最後に、特殊メソッドについても一応触れておきましょう。&や|には二つの集合の元から論理和、論理積をとって新たな集合を返す便利な特殊メソッドが対応しています。これはandやorにはない機能です。
ここでは、20未満の2の倍数からなる集合と3の倍数からなる集合の和集合、共通部分を求めてみましょう。
set_1 = {i for i in range(20) if i % 2 == 0}
set_2 = {i for i in range(20) if i % 3 == 0}
print(set_1 & set_2)
print(set_1 | set_2)
## このように書いてはならない!
print(set_1 and set_2)
print(set_1 or set_2)
結果は
{0, 18, 12, 6}
{0, 2, 3, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18}
{0, 3, 6, 9, 12, 15, 18}
{0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
でした。&や|では無事に集合の論理積、論理和の計算できましたが、andではset_2そのもの、orではset_1そのものが戻ってきてしまいました。これはまさにandやorはどちらかのオペラントをそのまま返すという性質を表しています。
if文ではandやorを使う場合が多く、普段あまり意識することではないかもしれませんが特殊メソッドを使うときは要注意です。
集合の演算にはビット単位演算の二項演算をつかう