知識整理：Pythonで演算子を混合する際の注意事項

# 初めに

とりあえず下記の論理式の結果がどうなるかを考える．

```py:
a = 1
b = 2
c = 3

print(a>b and c>a)
print(a>b or  c>a)
print(a>b  &  c>a)
print(a>b  |  c>a)
```

もちろん，a>bは偽であり，c>aは真である．
結果は以下の通り．

```py:
False
True
False
False
```

一番最後のFalseで，はて？となってしまった．
同じような間違いをしてしまった方向けの本記事．

## 起こっていること

試しに()をつけてみる．

```py:
print((a>b)  |  (c>a)) # -> True
```

これは期待通り．原因が分かってきた．
これならどうだ．

```py:
print(b | c) # -> 3
```

なるほど．つまりはこうなっていた．

```py:
print(a> b&c >a) # -> False
print(1>  2  >1) # -> False
print(a> b|c >a) # -> False
print(1>  3  >1) # -> False
```

# 知識① `&`や`|`はビット演算子，`and`や`or`はブール演算子

[こちらの記事](https://qiita.com/7shi/items/41d262ca11ea16d85abc)や[こちらの記事](https://qiita.com/Morio/items/ea3710b5411fc18e20d7)などからわかるように，今回混同してしまっていた2種類の演算子では明確に返り値が異なる．ブール演算子とはいえ，[こちらの記事](https://qiita.com/keisuke-nakata/items/e0598b2c13807f102469)のように，`and`や`or`の返り値はboolに変換されない形で定義されている．

整数値と組み合わせた際の挙動を下記に整理した（ビット演算子は他にもあるが，本記事では`&`と`|`のみを扱う）．

```py:
# 整数型の論理
print(bool(0)) # -> False
print(bool(1)) # -> True
print(bool(2)) # -> True

# T/Fとブール演算子，ビット演算子
print(False and True) # -> False
print(False or  True) # -> True
print(False  &  True) # -> False
print(False  |  True) # -> True

# 0/1とブール演算子，ビット演算子
print(0 and 1) # -> 0 (左がFalseなのでそれを返す)
print(0 or  1) # -> 1 (右のみがTrueなのでそれを返す)
print(0  &  1) # -> 0
print(0  |  1) # -> 1

# 2/1とブール演算子，ビット演算子
print(2 and 1) # -> 1 (左がTrueなので，真偽に関わらず右を返す)
print(2 or  1) # -> 2 (左がTrueなので，真偽に関わらず左を返す)
print(2  &  1) # -> 0 (二進法0010と0001の論理積0000)
print(2  |  1) # -> 3 (二進法0010と0001の論理和0011)
```



# 知識② 比較演算子`<``>`は複数連結できる

普段はほとんど使っていなかったものの，今回はこれが偶然に発生してしまった．

```py:
print(1 < 2 < 3) # -> True
print(1 < 3 < 2) # -> False
print(1 < 3 < 5 > 4 > 2) # -> True
```

# 知識③ 比較演算子`<``>`よりもビット演算子`&``|`の優先順位が高い

[こちらの記事](https://qiita.com/tTAGUCHI/items/1dee92cd058f975ffce3)にある表の通り，ビット演算子のほうが比較演算子よりも優先順位が高いため，本記事の事例では連結された比較演算子として扱われた．

**ビット演算子`&|` > 比較演算子`<>` > ブール演算子`and or`**

同種の演算子間での優先順位などは先述の記事を参照のこと．

------

# （追記）配列におけるブール/比較/ビット演算子

はじめにの議論からは離れるものの，配列に対する演算子の挙動は紛らわしい部分があるのでこちらも整理．配列に対する演算子の用途は一つに定まらない．今回は，「**配列の各要素に対して演算子を適用，ブール配列を返り値に欲しい**」，という立場で整理していく．

主な知識は[こちらの記事](https://www.haya-programming.com/entry/2019/03/15/235259)から．挙動が分かれる背景についても言及しており非常に助けになった．

# 知識④ list/tuple：ブール演算子`and``or`は使用可能，ビット演算子`&``|`は不可

ブール演算子は今回の目的通りの出力を返す．一方で，ビット演算子を使用しようとすると，サポートされていないというエラーメッセージが返ってくる．

```py:
# 使用配列
list_1  = [True, True,False,False]
list_2  = [True,False, True,False]
tuple_1 = (True, True,False,False)
tuple_2 = (True,False, True,False)

# ブール演算子
print( list_1 and  list_2) # [True, False, True, False]
print(tuple_1 and tuple_2) # (True, False, True, False)
print( list_1  or  list_2) # [True, True, False, False]
print(tuple_1  or tuple_2) # (True, True, False, False)

# ビット演算子（%を使用したことによるTypeErrorが返る）
print( list_1   &  list_2) # TypeError
print(tuple_1   & tuple_2) # TypeError
print( list_1   |  list_2) # TypeError
print(tuple_1   | tuple_2) # TypeError

# リストでの条件処理は困難（いずれも%を使用したことによるTypeErrorが返る）
print(list_1 % 2 == 0)
print(list_1 % ([2]*6) == 0)
```

listやtupleに対する（大小関係の）比較演算子についても言及しておくと，使用は可能である．ただし，返り値はブール配列でなくブール単値となる．大小関係は小さいindexからたどってはじめて判定された大小関係が出力される．使う機会はほぼない．

```py:
# 大小関係
print(a_list>[2]*6) # False (0番目どうし 0>2 の正誤，012345>222222の正誤ともいえる)
print(a_list<[2]*6) # True
print(a_list>[0,1,3,0,0,0]) # False (2番目どうし 2>3 の正誤，012345>013000の正誤ともいえる)
print(a_list>[0,1,1,5,5,5]) # True (2番目どうし 2>1 の正誤，012345>011555の正誤ともいえる)

# tupleもlistと同様
print(a_tuple>(2,)*6) # False
print(a_tuple<(2,)*6) # True
print(a_tuple>(0,1,3,0,0,0)) # False
print(a_tuple>(0,1,1,5,5,5)) # True
```

# 知識⑤ numpyには配列の論理演算用の関数が存在する

numpy配列であれば，条件文を柔軟に適用できる．

```py:
# 使用配列と条件文
import numpy as np
a = np.arange(6)

print(a%2==0) # [ True False  True False  True False]
print(a%3==0) # [ True False False  True False False]
```

これより，2つのブール配列に対する演算子の挙動を見ていく．ただ，結論としては，numpy配列のために作成されている論理演算関数を使用するのがよい．

```py:
# 配列のAND，OR処理
print(np.logical_and(a%2==0, a%3==0)) # [ True False False False False False]
print(np.logical_or( a%2==0, a%3==0)) # [ True False  True  True  True False]
```

# 知識⑥ numpy.ndarray：ブール演算子`and``or`は不可，ビット演算子`&``|`は可能

2つの配列に対し，ビット演算子の適用は可能である．しかしながら，下記の事例では，演算子の優先順位を考慮して()をつけている．

```py:
# 配列にビット演算子
print( a%2==0  &  a%3==0)  # ValueError
print( a%2==0  |  a%3==0)  # ValueError
print((a%2==0) & (a%3==0)) # [ True False False False False False]
print((a%2==0) | (a%3==0)) # [ True False  True  True  True False]
```

`==`は比較演算子の一つであり，先述の優先順位に組み込むと，

**ビット演算子`&|` > 比較演算子`<>``==` > ブール演算子`and or`**

である．そして，配列に対する`==`の複数連結はエラーを返す．
これを考慮すると，上の1行目の例は次のように分解される．

```py:
# print( a%2==0  &  a%3==0) の分解
x = a%2   # [0 1 0 1 0 1]
y = a%3   # [0 1 2 0 1 2]
z = 0 | y # [0 1 2 0 1 2]
print(x == z) # [ True  True False False False False]
print(z == 0) # [ True False False  True False False]
print(x == z == 0) # ValueError
```

さて，2つのブール配列に対してブール演算子を使用すると，()の使用に関わらずValueErrorが返ってくる．ダミー変数などの混じった配列に対して，えいやっ，で処理が進んで予期せぬ挙動をすることを防ぐ目的があると思われる．

```py:
# 配列に比較演算子
print( a%2==0  and  a%3==0)  # ValueError
print( a%2==0  or   a%3==0)  # ValueError
print((a%2==0) and (a%3==0)) # ValueError
print((a%2==0) or  (a%3==0)) # ValueError
```

ちなみに，算術演算子`+*`もビット演算子と同様に機能する．

```py:
# 配列に算術演算子
print( a%2==0  *  a%3==0)  # ValueError
print( a%2==0  +  a%3==0)  # ValueError
print((a%2==0) * (a%3==0)) # [ True False False False False False]
print((a%2==0) + (a%3==0)) # [ True False  True  True  True False]
```

蛇足ではあるが，このようなブール配列は，対象の配列内の特定要素のみに対して処理を（素早く）行うのに適している．

```py:
# ブール配列によるnumpy配列処理
print(a[a%2==0]) # [0 2 4]
a[a%2==0] = 999
print(a) # [999   1 999   3 999   5]
```

# 終わりに

今回整理した事項を意識していないと，予期せぬ挙動が平気で起こりうると考えられる．今後は注意を心掛けていきたい．