22
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MicroAd (マイクロアド)Advent Calendar 2021

Day 21

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

Last updated at Posted at 2021-12-20

はじめに

突然ですが、プログラミングの授業の期末試験だと思って、下の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)

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

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

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

うーん、たぶん a == bTruea is bFalse
と思って実行してみました。

>>> 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 では事前にメモリに格納されている値がある

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

Python ではメモリを節約するために、よく使われる値を事前にメモリに格納(interning)しているそうなのです。(*3)

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)

そんなとき、sys.intern(value)で自分の代入する値を既存の同値の値と同じメモリアドレスに配置することもできるそうですよ。(*5)

# 普通の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

22
3
0

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
22
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?