Python 比較演算子 'is' を使ったときの意外な落とし穴

# はじめに
突然ですが、プログラミングの授業の期末試験だと思って、下のPythonコードの出力結果を考えてみてください。

```
a = 100
b = 100
print('a == b: ', a == b)
print('a is b: ', a is b)

a = 300
b = 300
print('a == b: ', a == b)
print('a is b: ', a is b)
```

今回は上のような比較演算子の振る舞いについて、Pythonビギナーが知ったつもりになって知らなかった小ネタを書き留めておきたいと思います。

# この記事のまとめ

- Python では、int の `-5` ~ `256` の値がそれぞれ同じメモリアドレスに格納されている
- 同じメモリアドレスに格納されている int とそうでないものでオブジェクト比較時の振る舞いが変わる
- `intern(value)` でよく使う値を同じメモリアドレスに格納することもできる

# 'is' is not '=='

`is` と `==` は Python でよく使われる比較演算子の代表です。
プログラミングの本では、

- `is` は同一のオブジェクトかの比較
- `==` は値が等しいかの比較

みたいに勉強した記憶があります。
以下のようなコードがよく例に出されます。

```
>>> a = 'Hello!'
>>> b = 'Hello!'
>>> print(a == b)
True
>>> print(a is b)
False
```
この例は

- a と b はどちらも同じ 'Hello!' という文字列を持っているから、`==` の値比較は `True` を返すよ！
- でも、a と b はメモリ上で違う場所に格納されているから `is` の比較では `False` を返すよ！

ってことを示しています。

# ビギナーを陥れた落とし穴

Python の仕様では、int 型の値は str 型の値と同じでイミュータブル（代入後に変更不可）だったはず。([*1](https://gammasoft.jp/blog/python-built-in-types/#type-table))

つまり、変数に値を代入するときには新しいメモリのアドレスに値を割り当てているはず！（名推理）

ここで冒頭の問題に巻き戻します。
下のコードは何を表示すると思いますか？

```
a = 100
b = 100
print('a == b: ', a == b)
print('a is b: ', a is b)
```
うーん、たぶん `a == b` が `True` で `a is b` が `False`！
と思って実行してみました。

```
>>> a = 100
>>> b = 100
>>> print('a == b: ', a == b)
a == b:  True
>>> print('a is b: ', a is b)
a is b:  True
```

え、int型は同じ値が同一オブジェクトとして評価されるんだ！
じゃあ `is` 演算子は int 型に関しては `==` の代わりとして使えちゃうじゃん！

ここで冒頭の問題の残りの部分です。
下のコードは何を表示するでしょう？

```
a = 300
b = 300
print('a == b: ', a == b)
print('a is b: ', a is b)
```
うーん、これはどっちも `True`！
と思って実行してみました。

```
>>> a = 300
>>> b = 300
>>> print('a == b: ', a == b)
a == b:  True
>>> print('a is b: ', a is b)
a is b:  False
```

おっと、`100`を代入したときと振る舞いが違います？？

今の結果をまとめるとこんな感じです。

| a と b の値  | a == b | a is b |
|:-:|:-:|:-:|
| 100 | True | True |
| 300 | True | False |

# Python では事前にメモリに格納されている値がある

どういうことかと調べてみた結果、どうやら `a` と `b` が `100` だったときの振る舞いは Python の最適化による副作用だったようです。

Python ではメモリを節約するために、よく使われる値を事前にメモリに格納（interning)しているそうなのです。([*3](https://towardsdatascience.com/optimization-in-python-interning-805be5e9fd3e))

int 型であれば `-5` <= a <= `256` までの値が事前に特定のメモリアドレスに格納されているとのこと。

だから`256` 以下の数字である `100` を変数に代入しようとすると、すでに特定のアドレスに格納されている `100` の int オブジェクトを参照するようになるので、`is` のオブジェクト比較は `True` になります。
一方、それより大きい `300` の値は特定のアドレスに格納されていないので、新しいメモリアドレスに割り当てられた int オブジェクトを参照するようになり、`is`のオブジェクト比較は `False` になったわけです。

```
>>> a = -6
>>> b = -6
>>> print(a, ' == ', b, ': ', a == b)
-6  ==  -6 :  True
>>> print(a, ' is ', b, ': ', a is b)
-6  is  -6 :  False

>>> a = -5
>>> b = -5
>>> print(a, ' == ', b, ': ', a == b)
-5  ==  -5 :  True
>>> print(a, ' is ', b, ': ', a is b)
-5  is  -5 :  True

>>> a = 0
>>> b = 0
>>> print(a, ' == ', b, ': ', a == b)
0  ==  0 :  True
>>> print(a, ' is ', b, ': ', a is b)
0  is  0 :  True

>>> a = 256
>>> b = 256
>>> print(a, ' == ', b, ': ', a == b)
256  ==  256 :  True
>>> print(a, ' is ', b, ': ', a is b)
256  is  256 :  True

>>> a = 257
>>> b = 257
>>> print(a, ' == ', b, ': ', a == b)
257  ==  257 :  True
>>> print(a, ' is ', b, ': ', a is b)
257  is  257 :  False
```

まとめると以下のようになります。

| a と b の値 | a == b | a is b |
|:-:|:-:|:-:|
| -5 未満 | True | False |
| -5 以上 256 以下 | True | True |
| 256 より大きい | True | False |

結局のところ、`is` 演算子を値の比較に使えるかと思い込んでしまうのが誤りなんですが、値の大きさによって振る舞いが違ったっていう意外な発見です。

# 値を同じアドレスに格納する

でも時によって「よく使う値」なんて変わるじゃないですか。
もしかしたら今回のプログラムでは"$${ctx:loginId}"みたいな文字列が頻繁に使われるかもしれないでしょう？

比較する文字列が特に長い場合、値一致を比較する`==`よりアドレス一致を確認する`is`の方が比較速度が早いということもあるそうです。([*6](https://qiita.com/amedama/items/66b5f797326198b51a2f))

そんなとき、`sys.intern(value)`で自分の代入する値を既存の同値の値と同じメモリアドレスに配置することもできるそうですよ。([*5](https://www.codesansar.com/python-programming/string-interning.htm))

```
# 普通のstrのis比較ではFalseになる
>>> a = "chocolate pie"
>>> b = "chocolate pie"
>>> print(a is b)
False

# internで特定のメモリアドレスに格納した値はis比較でTrueになる
>>> import sys
>>> a = sys.intern("chocolate pie")
>>> b = sys.intern("chocolate pie")
>>> print(a is b)
True

# internで格納していない値との比較ではFalseになる
>>> c = "chocolate pie"
>>> print(a is c)
False
```

頭の片隅に置いといたら、思い出したときにちょっとだけ実行速度が速くなるかもしれないですね。

# おわりに

というわけで、is演算子での比較でハマったネタを書いてみました。
Python っておもしろいね！って思ってもらえたら嬉しいです。

そして、この仕様を知らずに3時間以上無駄にしてしまった自分のようなビギナーのお役に立てたら、記事を書いた甲斐があるというものです。

お読みいただきありがとうございます。

# おまけ

`a = b = 値` で代入したらどうなるでしょう？

```
>>> a = 512
>>> b = 512
>>> print(a is b)
False
>>> a = b = 512
>>> print(a is b)
True
```

同じアドレスに格納されるみたいですね！

# 参考文献

*1. GAMMASOFT, Pythonの組み込みデータ型の分類表（ミュータブル等）, https://gammasoft.jp/blog/python-built-in-types/#type-table

*2. Joska de Langen, Python '!=' Is Not 'is not': Comparing Objects in Python, https://realpython.com/python-is-identity-vs-equality/

*3. Chetan Ambi, Optimization in Python — Interning, https://towardsdatascience.com/optimization-in-python-interning-805be5e9fd3e

*4. Muhammad Hashir Hassan, Guide to String Interning in Python, https://stackabuse.com/guide-to-string-interning-in-python/

*5. CodeSansar, String Interning in Python (Optimization), https://www.codesansar.com/python-programming/string-interning.htm

*6, amedama, Python sys.intern(), https://qiita.com/amedama/items/66b5f797326198b51a2f