目次
はじめに
Pythonの真偽値評価には==
とis
がありますよね。筆者はこれらの使い分けとして、None
にはis
を使う程度しかしておらず、正直よくわかっていませんでした。そこで、色々な型を様々な操作と組み合わせつつ==
やis
で比較し、何か使えそうな場面がないか探り、ついでに新たなオブジェクトが生成されるタイミングについても探っていければと思います。
動作環境によって、記事内の挙動と違う挙動をする可能性があります。筆者の動作環境は以下の通りです。
OS 名: Microsoft Windows 11 Home
OS バージョン: 10.0.22631 N/A ビルド 22631
Python バージョン: 3.11.3
開発環境: IDLE (Python3.11.3)
結果
とりあえず、異なる型から型変換した場合は、bool型
を除く全ての型で、新しくオブジェクトが生成されているようでした。同じ型で型変換する場合には、ミュータブルな型と、イミュータブルな型で挙動が分かれたので分けて紹介します。
イミュータブル
#整数
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
これらの型では、同一の型で型変換した場合には、新しくオブジェクトが生成されませんでした。これらの型では、リストでよく使われるような、スライス表記で全体をコピーする手法でも、新しくオブジェクトの生成が行われませんでした。
ミュータブル
#同じ要素を持つリスト
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の整数については、他の整数と異なる挙動を取るので注意が必要です。
#-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
#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
を文字列として打ち込んでも特別扱いされず、普通に空文字でない文字列として扱われますね。
おまけ:インスタンスと関数
一応やってみましたがまあ当然ですね
#同じ引数を渡したインスタンス
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__
メソッドのオーバーライドを用いれば、インスタンスの比較が可能になります。
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
を返すような、自作のリストとか作ってみたら面白いかもしれません。
おまけ2:namedtuple
個人的に気になったのでnamedtuple
についてもちょっと試してみました。
#同じ要素を持つ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
を返すんですね。この辺はどうなるのか正直予想ついてなかったんですが、やってみると意外と面白いですね。効果的に使えそうな場面は特に浮かびませんが
おまけ3:計算を含む整数
#計算を含む整数
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型
を経由したか調べるのに使えるのか...?そんなことをしたい場面があるのか...?)
おまけ4:copyモジュールについて
copy.copy
とcopy.deepcopy
についてです。とりあえずリストに絞って話します。copy.deepcopy
は元のオブジェクトに影響を与えたくない時に使われるイメージ通り、新しくオブジェクトを生成します。筆者は勘違いしていたのですが、copy.copy
についても、新しくオブジェクトの生成が行われているようです。じゃあなんでcopy.copy
で作った方は書き換えると、元のオブジェクトも変わんだよ!と思ったんですが、この点について、コメントで的確な説明をいただいたので、ここで紹介させていただきます。
copy.deepcopy
は深いコピーであり、すべての階層をコピーするのに対し、copy.copy
は浅いコピー(シャローコピー)であり、1階層だけコピーして2階層目以降はコピーせずに共有する。
>>> 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
を返すという事ですね。
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
でもオブジェクトの生成は行われませんでした。
おまけ5:文字列の結合
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において、イミュータブルなオブジェクトを生成する際には、このインターンが行われているようです。これがイミュータブルな型とミュータブルな型で挙動が異なった理由です。
インターンについては、こちらの記事に詳しく書いてありました。また、先ほどのおまけ5の内容についてもこちらに載っていました。
このインターンですが、文字列であればsys.intern
を用いて、手動で設定することも可能です、これを設定した場合の挙動は以下のようになります。
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
インターンを手動で設定したもののみが、同じメモリを参照するようになっています。宣言したものすべてが同じメモリを指すようになるわけではないので、注意してください。
ネタ:インターンの回避
ちなみにですが、逆にインターンを手動で回避する方法は提供されていないようです。必要な場面があるか分かりませんが、そういった操作が必要になる場合は、先ほどの結果から分かるように、一度別の型に変換してから、元の型に戻すことで実現可能です。文字列については、他にもいろいろ方法があって面白かったので、ここでまとめて書いておきます。
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
という基本の使い分けを徹底するのが、結局一番大事と結論付けておく。
最後に
長くなりましたが、ここまで読んで下さりありがとうございました。正直ネタよりの記事な感じはしますが、ニッチな需要にハマって役に立てばと思います。個人的には、何の役に立つかは分からないけど、へ~そういう挙動になるんだ~って感じで面白かったので満足です。
自分の環境でも試したいという人のために、記事内のコードをまとめたものを置いておきます
クリックして展開
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