1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-12

目次

はじめに

 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

TrueFalseは他の型とは異なり、どの型から変換されようが、必ず同じ識別子を持つようですね。また、当然ですが、FalseNoneを文字列として打ち込んでも特別扱いされず、普通に空文字でない文字列として扱われますね。

おまけ:インスタンスと関数

 一応やってみましたがまあ当然ですね

#同じ引数を渡したインスタンス
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以外の整数は型変換すると、新しくオブジェクトが生成されるとお話ししました。これに伴ってdeのケースのように計算途中でfloat型になるような場合には、新しくオブジェクトが生成されてしまうので、isの比較は計算結果の確認として使えません。わざわざ数値計算でisを使う機会などないと思いますが、やはり必要なければ==を使う用に徹底すべきですね。(逆に言えば計算途中にfloat型を経由したか調べるのに使えるのか...?そんなことをしたい場面があるのか...?)

おまけ4:copyモジュールについて

 copy.copycopy.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
1
1
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?