【真偽値評価】色々な型のものを==とisで比較してみた 

## 目次 
- [はじめに](#はじめに)
- [結果](#結果)
  - [イミュータブル](#イミュータブル)
  - [ミュータブル](#ミュータブル)
  - [-5～256の整数](#-5256の整数)
  - [boolとNone](#boolとnone) 
  - [おまけ：インスタンスと関数](#おまけインスタンスと関数) 
  - [おまけ２：namedtuple](#おまけ２namedtuple) 
  - [おまけ３：計算を含む整数](#おまけ３計算を含む整数) 
  - [おまけ４：copyモジュールについて](#おまけ４copyモジュールについて) 
  - [おまけ５：文字列の結合](#おまけ５文字列の結合) 
- [なぜ型によって違いがあるのか](#なぜ型によって違いがあるのか)
- [まとめ](#まとめ) 
- [最後に](#最後に)

## はじめに
　Pythonの真偽値評価には```==```と```is```がありますよね。筆者はこれらの使い分けとして、```None```には```is```を使う程度しかしておらず、正直よくわかっていませんでした。そこで、色々な型を様々な操作と組み合わせつつ```==```や```is```で比較し、何か使えそうな場面がないか探り、ついでに新たなオブジェクトが生成されるタイミングについても探っていければと思います。
 
:::note warn
動作環境によって、記事内の挙動と違う挙動をする可能性があります。筆者の動作環境は以下の通りです。
```
OS 名:                  Microsoft Windows 11 Home
OS バージョン:          10.0.22631 N/A ビルド 22631
Python バージョン:      3.11.3
開発環境:               IDLE (Python3.11.3)
```
:::
## 結果
　とりあえず、異なる型から型変換した場合は、```bool型```を除く全ての型で、新しくオブジェクトが生成されているようでした。同じ型で型変換する場合には、ミュータブルな型と、イミュータブルな型で挙動が分かれたので分けて紹介します。
### イミュータブル
```python
#整数
a = 12345
b = 12345
c = int(a)
d = int('12345')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False

#小数
a = 12.345
b = 12.345
c = float(a)
d = float('12.345')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False

#同じ要素を持つタプル
a = (1, 2, 3, 4, 5)
b = (1, 2, 3, 4, 5)
c = tuple(a)
d = a[:]
e = tuple([1, 2, 3, 4, 5])
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True
print(a == e, a is e) #True, False

#文字列
a = '12345'
b = '12345'
c = str(a)
d = a[:]
e = str(12345)
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True
print(a == e, a is e) #True, False
```
これらの型では、同一の型で型変換した場合には、新しくオブジェクトが生成されませんでした。これらの型では、リストでよく使われるような、スライス表記で全体をコピーする手法でも、新しくオブジェクトの生成が行われませんでした。
### ミュータブル
```python
#同じ要素を持つリスト
a = [1, 2, 3, 4, 5]
b = [1, 2, 3, 4, 5]
c = list(a)
d = a[:]
print(a ==  b, a is b) #True, False
print(a ==  c, a is c) #True, False
print(a ==  d, a is d) #True, False

#同じ要素を持つ集合
a = {1, 2, 3, 4, 5}
b = {1, 2, 3, 4, 5}
c = set(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False

#同じ要素を持つ辞書
a = {'one': 1, 'Two': 2, 'Three': 3}
b = {'one': 1, 'Two': 2, 'Three': 3}
c = dict(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False
```
これらの型では、同一の型で型変換するだけで、新しくオブジェクトが生成されました。また、これはよく使われる手法なので、みなさんご存じとは思いますが、リストではスライス表記を使ったコピーでも同じことができました。

### -5～256の整数
-5～256の整数については、他の整数と異なる挙動を取るので注意が必要です。
```python
#-5～256の整数
a = 123
b = 123
c = int(a)
d = int('123')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True
```
このように、他の型から型変換した場合にも、同じ識別子が与えられます。気になる人は、-6, -5, 256, 257と数字を変えて試してみてください。
### boolとNone
```python
#bool
a = False
b = False
c = bool(a)
d = bool('False')
e = bool('')
f = bool(0)
g = bool([])
h = bool(None)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, True
print(a == d, a is d)#False, False
print(a == e, a is e)#True, True
print(a == f, a is f)#True, True
print(a == g, a is g)#True, True
print(a == h, a is h)#True, True

#None
a = None
b = None
c = bool('None')
print(a == b, a is b)#True, True
print(a == c, a is c)#False, False
```
```True```や```False```は他の型とは異なり、どの型から変換されようが、必ず同じ識別子を持つようですね。また、当然ですが、```False```も```None```を文字列として打ち込んでも特別扱いされず、普通に空文字でない文字列として扱われますね。
### おまけ：インスタンスと関数
　一応やってみましたがまあ当然ですね
```python
#同じ引数を渡したインスタンス
class Class:
    def __init__(self, one, two, three):
        self.one = one
        self.two = two
        self.three = three
a = Class(1, 2, 3)
b = Class(1, 2, 3)
print(a == b, a is b) #False, False

#同じ処理をする関数
def funcA():
    pass
def funcB():
    pass

a = funcA
b = funcB
print(a == b, a is b) #False, False
```
ちなみに、データクラスや、```__eq__```メソッドのオーバーライドを用いれば、インスタンスの比較が可能になります。
```python
from dataclasses import dataclass
@dataclass
class DataClass:
    one: int
    two: int
    three: int
a = DataClass(1, 2, 3)
b = DataClass(1, 2, 3)
print(a == b, a is b) #True, False

class CustomClass:
    def __init__(self, one, two, three):
        self.one = one
        self.two = two
        self.three = three
    def __eq__(self, other):
        if isinstance(other, CustomClass):
            return self.one == other.one and self.two == other.two and self.three == other.three
a = CustomClass(1, 2, 3)
b = CustomClass(1, 2, 3)
print(a == b, a is b) #True, False
```
上のコードを見ればわかりますが、```__eq__```をオーバーライドするときに、条件を変えれば、また別の条件でTrueを返すようにできます。一つでも一致すれば```True```を返すとか、何か使い道があるかもしれませんね。リストを継承して、順番が違っても同じ要素なら```True```を返すような、自作のリストとか作ってみたら面白いかもしれません。
### おまけ２：namedtuple
　個人的に気になったので```namedtuple```についてもちょっと試してみました。
```python
#同じ要素を持つnamedtupleとタプル
from collections import namedtuple
NamedTuple = namedtuple('NamedTuple', ['one', 'two', 'three'])
a = NamedTuple(1, 2, 3)
b = NamedTuple(1, 2, 3)
c = (1, 2, 3)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False
```
タプルを基に作られてはいるものの、同じ中身を持っているからと言って同じ```id```を参照するわけではないみたいですね。しかしタプルを基に作られているので、通常のタプルとの比較でも```==```は```True```を返すんですね。この辺はどうなるのか正直予想ついてなかったんですが、やってみると意外と面白いですね。~~効果的に使えそうな場面は特に浮かびませんが~~

### おまけ３：計算を含む整数
```python
#計算を含む整数
a = 12345
b = int(12346 - 1)
c = int(24690 // 2)
d = int(12346 - 1.0)
e = int(24690 / 2)
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False
print(a == e, a is e) #True, False
```
-5～256以外の整数は型変換すると、新しくオブジェクトが生成されるとお話ししました。これに伴って```d```、```e```のケースのように計算途中で```float```型になるような場合には、新しくオブジェクトが生成されてしまうので、```is```の比較は計算結果の確認として使えません。わざわざ数値計算で```is```を使う機会などないと思いますが、やはり***必要なければ```==```を使う用に徹底すべき***ですね。（逆に言えば計算途中に```float型```を経由したか調べるのに使えるのか...？そんなことをしたい場面があるのか...？）

### おまけ４：copyモジュールについて
 　```copy.copy```と```copy.deepcopy```についてです。とりあえずリストに絞って話します。```copy.deepcopy```は元のオブジェクトに影響を与えたくない時に使われるイメージ通り、新しくオブジェクトを生成します。筆者は勘違いしていたのですが、***```copy.copy```についても、新しくオブジェクトの生成が行われている***ようです。じゃあなんで```copy.copy```で作った方は書き換えると、元のオブジェクトも変わんだよ！と思ったんですが、この点について、コメントで的確な説明をいただいたので、ここで紹介させていただきます。
```copy.deepcopy```は深いコピーであり、すべての階層をコピーするのに対し、```copy.copy``` は浅いコピー（シャローコピー）であり、1階層だけコピーして2階層目以降はコピーせずに共有する。

```python
>>> import copy
>>> a = [1,[2],[[3]]]
>>> b = copy.copy(a)
>>> a[0] = 4        # 1階層目への代入は影響せず
>>> a[1][0] = 5     # 2階層目は共有しているので影響する
>>> a[2][0][0] = 6  # 3階層目も当然影響
3
>>> b
[1, [5], [[6]]]
```
つまりいずれの場合も、一階層目のコピーは行われるので、以下のように```is```で比較すると```False```を返すという事ですね。
```python
import copy
#タプルなど
a = (1, 2, 3)
b = copy.copy(a)
c = copy.deepcopy(a)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, True
#リストなど
a = [1, 2, 3]
b = copy.copy(a)
c = copy.deepcopy(a)
print(a == b, a is b)#True, False
print(a == c, a is c)#True, False
 ```
 リスト以外に話を広げます。ミュータブルな型はリストと同じ挙動を取ります。しかし、上のコードから分かるように、タプルなど、イミュータブルな型は、```copy.copy```でも```copy.deepcopy```でもオブジェクトの生成は行われませんでした。

### おまけ５：文字列の結合
```python
a = '12345'
b = '12'+ '345'
c = '12'+ str('345')
d = '12'+ str(345)
e = a + str('')
print('345' is str('345')) #True
print(b is c) #False
print(a == b, a is b) #True, True
print(a == c, a is c) #True, False
print(a == d, a is d) #True, False
print(a == e, a is e) #True, True
```
```print('345' is str('345'))```が```True```なのに```print(b is c)```が```False```なのが意外でした。にもかかわらず、```print(a is e)```に関しては```True```なので、空文字列は最適化処理が違うみたいですね。

## なぜ型によって違いがあるのか
　型によって挙動が違うのは、インターンという技術が関わっているようです。インターンとは、***オブジェクト生成時に、すでに同じ値のものがあれば、同じメモリを参照するようにするもの***です。これにより、メモリの節約などが期待できます。Pythonにおいて、イミュータブルなオブジェクトを生成する際には、このインターンが行われているようです。これがイミュータブルな型とミュータブルな型で挙動が異なった理由です。
 インターンについては、こちらの記事に詳しく書いてありました。また、先ほどのおまけ５の内容についてもこちらに載っていました。
 
 https://qiita.com/kahibella/items/cd8b48f1d2c109b76e10

このインターンですが、文字列であれば```sys.intern```を用いて、手動で設定することも可能です、これを設定した場合の挙動は以下のようになります。
```python
import sys
a = '12345'
b = sys.intern(str(12345))
c = str(12345)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, False
```
***インターンを手動で設定したもののみが、同じメモリを参照する***ようになっています。宣言したものすべてが同じメモリを指すようになるわけではないので、注意してください。
### ネタ：インターンの回避
　ちなみにですが、逆に***インターンを手動で回避する方法は提供されていない***ようです。必要な場面があるか分かりませんが、そういった操作が必要になる場合は、先ほどの結果から分かるように、一度別の型に変換してから、元の型に戻すことで実現可能です。文字列については、他にもいろいろ方法があって面白かったので、ここでまとめて書いておきます。
```python
a = '12345'
b = bytearray(a, 'utf-8').decode('utf-8')
c = a[:-1] + str(a[-1])
d = ''.join(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False
print(a == d, a is d) #True, False
```

## まとめ
　イミュータブルなオブジェクトにはインターンが適用されるため、オブジェクトの生成時に、ミュータブルなオブジェクトとは挙動が変わることが分かった。インターンは文字列については、```sys.intern```を用いて手動でも適用できる。逆にインターンを回避するような関数は提供されていないが、```bool型```以外は、型変換を活用すれば実現が可能である。
　はじめにの項目で探ると宣言した、```==```と```is```の使い分けについては、同値かを調べるなら```==```、同じオブジェクト化を調べるなら```is```という***基本の使い分けを徹底するのが、結局一番大事***と結論付けておく。

## 最後に
　長くなりましたが、ここまで読んで下さりありがとうございました。正直ネタよりの記事な感じはしますが、ニッチな需要にハマって役に立てばと思います。個人的には、何の役に立つかは分からないけど、へ～そういう挙動になるんだ～って感じで面白かったので満足です。
 
自分の環境でも試したいという人のために、記事内のコードをまとめたものを置いておきます
<details> 
<summary>クリックして展開</summary>
<pre>
import copy, sys
from dataclasses import dataclass
from collections import namedtuple
print('イミュータブル')
#整数
a = 12345
b = 12345
c = int(a)
d = int('12345')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False

#小数
a = 12.345
b = 12.345
c = float(a)
d = float('12.345')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False

#同じ要素を持つタプル
a = (1, 2, 3, 4, 5)
b = (1, 2, 3, 4, 5)
c = tuple(a)
d = a[:]
e = tuple([1, 2, 3, 4, 5])
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True
print(a == e, a is e) #True, False

#文字列
a = '12345'
b = '12345'
c = str(a)
d = a[:]
e = str(12345)
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True
print(a == e, a is e) #True, False

print('ミュータブル')
#同じ要素を持つリスト
a = [1, 2, 3, 4, 5]
b = [1, 2, 3, 4, 5]
c = list(a)
d = a[:]
print(a ==  b, a is b) #True, False
print(a ==  c, a is c) #True, False
print(a ==  d, a is d) #True, False

#同じ要素を持つ集合
a = {1, 2, 3, 4, 5}
b = {1, 2, 3, 4, 5}
c = set(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False

#同じ要素を持つ辞書
a = {'one': 1, 'Two': 2, 'Three': 3}
b = {'one': 1, 'Two': 2, 'Three': 3}
c = dict(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False

print('-5～256の整数')
#-5～256の整数
a = 123
b = 123
c = int(a)
d = int('123')
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, True

print('boolとNone')
#bool
a = False
b = False
c = bool(a)
d = bool('False')
e = bool('')
f = bool(0)
g = bool([])
h = bool(None)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, True
print(a == d, a is d)#False, False
print(a == e, a is e)#True, True
print(a == f, a is f)#True, True
print(a == g, a is g)#True, True
print(a == h, a is h)#True, True

#None
a = None
b = None
c = bool('None')
print(a == b, a is b)#True, True
print(a == c, a is c)#False, False

print('インスタンスと関数')
#同じ引数を渡したインスタンス
class Class:
    def __init__(self, one, two, three):
        self.one = one
        self.two = two
        self.three = three
a = Class(1, 2, 3)
b = Class(1, 2, 3)
print(a == b, a is b) #False, False

#同じ処理をする関数
def funcA():
    pass
def funcB():
    pass

a = funcA
b = funcB
print(a == b, a is b) #False, False

#データクラス
@dataclass
class DataClass:
    one: int
    two: int
    three: int
a = DataClass(1, 2, 3)
b = DataClass(1, 2, 3)
print(a == b, a is b) #True, False

#自作クラス
class CustomClass:
    def __init__(self, one, two, three):
        self.one = one
        self.two = two
        self.three = three
    def __eq__(self, other):
        if isinstance(other, CustomClass):
            return self.one == other.one and self.two == other.two and self.three == other.three
a = CustomClass(1, 2, 3)
b = CustomClass(1, 2, 3)
print(a == b, a is b) #True, False

print('namedtuple')
#同じ要素を持つnamedtupleとタプル
NamedTuple = namedtuple('NamedTuple', ['one', 'two', 'three'])
a = NamedTuple(1, 2, 3)
b = NamedTuple(1, 2, 3)
c = (1, 2, 3)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False

print('計算を含む整数')
#計算を含む整数
a = 12345
b = int(12346 - 1)
c = int(24690 // 2)
d = int(12346 - 1.0)
e = int(24690 / 2)
print(a == b, a is b) #True, True
print(a == c, a is c) #True, True
print(a == d, a is d) #True, False
print(a == e, a is e) #True, False

print('copyモジュール')
#タプルなど
a = (1, 2, 3)
b = copy.copy(a)
c = copy.deepcopy(a)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, True
#リストなど
a = [1, 2, 3]
b = copy.copy(a)
c = copy.deepcopy(a)
print(a == b, a is b)#True, False
print(a == c, a is c)#True, False

print('文字列の結合')
a = '12345'
b = '12'+ '345'
c = '12'+ str('345')
d = '12'+ str(345)
e = a + str('')
print('345' is str('345')) #True
print(b is c) #False
print(a == b, a is b) #True, True
print(a == c, a is c) #True, False
print(a == d, a is d) #True, False
print(a == e, a is e) #True, True

print('インターン')
a = '12345'
b = sys.intern(str(12345))
c = str(12345)
print(a == b, a is b)#True, True
print(a == c, a is c)#True, False

print('文字列のインターンの回避')
a = '12345'
b = bytearray(a, 'utf-8').decode('utf-8')
c = a[:-1] + str(a[-1])
d = ''.join(a)
print(a == b, a is b) #True, False
print(a == c, a is c) #True, False
print(a == d, a is d) #True, False
</pre>
</details>