Pythonで2つの文字列がa == bだけどもnot(a is b)であるようなケース

文字列の比較で疑問から引用:

d1 = 'a a'
d2 = 'a a'

id(d1)
# => 27605856
id(d2)
# => 27606080

えっなんで?

文字列は変更不可能なオブジェクトなので普通はisじゃなくて==で比較をするんだけどね。空白で区切られた同一内容の文字列を2つ、対話的環境で個別に作成してからisで比較するとFalseになるようだ。

Python 2.6.1 (r261:67515, Jan 25 2009, 00:21:48) 
[GCC 4.0.1 (Apple Inc. build 5490)] on darwin
>>> a = "a a"
>>> b = "a a"
>>> a is b
False

しかし、一つの式で書くとTrue

>>> "a a" is "a a"
True

一つの関数にまとめてもTrue

>>> def foo():
...     a = "a a"
...     b = "a a"
...     return a is b
... 
>>> foo()
True

一つの行にまとめてもTrue

>>> a = "a a"; b = "a a"; a is b
True

対話的環境で1行ずつ実行する際には、1行ごとにバイトコードへのコンパイルが行われている。関数を作ったり、1行で実行したりしている際にはまとめてコンパイルされている。そこの違いだろう。

「同じ内容の文字列は同じオブジェクトにする」というのを徹底的にやろうとすると文字列が作成される度に「これって今までに現れた文字列のどれかと一緒だろうか?」をチェックする必要が出てくるよね。それって結構大変な処理だし、Pythonの文字列はそもそも変更不可能なオブジェクトなので徹底する必要がないんだ。だから高速化のためにちょっと手を抜いてあるんだろう。なんだろうね。「アルファベットと数字以外の文字を含んでいるような文字列はまあ識別子にもなれないことだし同一内容の文字列がある確率が低いんじゃないか」とかかな?

>>> a = "a@"
>>> b = "a@"
>>> a is b
False
>>> a = "a!"
>>> b = "a!"
>>> a is b
False
>>> a = "a1"
>>> b = "a1"
>>> a is b
True

もっと詳しく知りたい場合は、文字列を作って返すためのビルダー関数の中で「すでにある文字列だったらそれへのポインタ、そうでなかったら新しく作ってそれへのポインタを返そう」って感じで分岐をしたりとかしていると思われるのでそこからコードをたどってみればいいかと思う。