is演算子のふしぎ

Pythonには、二つのオブジェクトが同じオブジェクトかどうか判定する is演算子というのがある。==演算子とちょっと似ているが、==演算子は二つのオブジェクトのが等しいかどうかを判定し、is演算子値に関わらず異なるオブジェクトが指定されればFalseを返す。

>>> list1 = [1,2,3]
>>> also_list1 = list1
>>> list2 = [2,3,4]
>>> equal_to_list1 = [1,2,3]
>>> list1 is also_list1		# list1 と also_list1 は同じオブジェクト
True
>>> list1 is list2		# list1 と list2 は異なるオブジェクト
False
>>> list1 == list2		# list1 と list2 は異なる値を持つ
False
>>> list1 is equal_to_list1	# list1 と equal_to_list1は異なるオブジェクト
False
>>> list1 == equal_to_list1	# list1 と equal_to_list1 は同じ値を持つ
True

とまぁ、ここまでは簡単なのだが、いざis演算子を書いて実際に動作を確認しようとすると、ちょっと混乱してしまう場合がある。

例えば、

>>> a = 0
>>> b = 0
>>> print (a is b)

は、どんな結果を返すだろうか?is演算子には二つの数値オブジェクトが指定されているので、素直に考えれば結果はFalseとなるだろう。

実は、Pythonの言語仕様上では、上の式はTrueともFalseとも予測できない。ただ、現在のCPythonの実装では、

>>> a = 0
>>> b = 0
>>> print (a is b)
True

Trueとなる。

では、

>>> a = 0.1
>>> b = 0.1
>>> print (a is b)

はどうだろうか? 上の整数を使った場合と異なり、この場合にはFalseとなる。だが、

>>> def test_float():
...     a = 0.1
...     b = 0.1
...     print (a is b)
...
>>> test_float()

はどうだろう?不思議なことに、関数内で実行すると、まったく同じ式が今度はTrueとなってしまうのだ。

また、

>>> 0.1 is 0.1

Trueとなる。

Pythonはオブジェクトを再利用する

Pythonでは、出来るだけ不要なオブジェクトを生成しないよう、色々と工夫されている。整数の 0 のように、あちこちで大量に消費されるオブジェクトをいちいち作ったり消したりしていては、メモリ消費が増大し、パフォーマンスも大きく低下してしまうだろう。そこで、更新不能なオブジェクトで、使用頻度が高いものについては、一度作ったらその後同じオブジェクトをずっと使い回すようになっているのだ。このため、一見別々のオブジェクトのように見えても、実は同じオブジェクトだった、ということになるのである。

このような使い回しがどのように行われるかはPythonの実装詳細であり、どのように行われるかは規定されていない。CPythonとJythonでは異なるだろうし、CPythonでもバージョンが変わればまた別の挙動を示したりする。いちいち調べたり憶えたりしないで、「そういうもの」として理解していただきたい。決して、Pythonの再利用アルゴリズムに依存したコードを書いてはならない。

現在のCPython2.xで再利用される代表的なオブジェクトに、他にタプルと文字列がある。タプルの場合、長さ0のタプルは常に再利用される。

>>> a = ()
>>> b = ()
>>> print (a is b)
True

また、長さ1以下の文字とユニコード文字も再利用される。

>>> a = 'a'
>>> b = 'a'
>>> print (a is b)
True

文字列の共有

もう一つ、不思議な例を紹介しよう。

>>> a = "dead parrot"
>>> b = "dead parrot"
>>> print (a is b)

これは、Falseを返す。"dead parrot" は長さ1の文字列ではないので、共有されないからだ。

ところが、

>>> a = "dead_parrot"
>>> b = "dead_parrot"
>>> print (a is b)

Trueを返すのである。違いはdeadparrotの間のスペースだけだ。何故だろうか?

dead_parrotのように、アルファベットと数字、_ だけで構成される文字列リテラルは、全てintern化されるようになっている。

intern化とは、二つの文字列s1s2があったとき、s1 == s2 ならば常に s1 is s2 が成立するように処置することだ。組み込み関数 intern()で文字列をintern化することができる。

>>> a = "dead parrot"
>>> b = "dead parrot"
>>> a is b
False
>>> a_interned = intern(a)
>>> b_interned = intern(b)
>>> a_interned is b_interned
True

文字列をintern化することにより、文字列の比較を高速に行うことが出来るようになる。実際に一文字一文字比較せずに、is演算子で同じオブジェクトかどうかだけをチェックすれば済むようになるからだ。英数字だけで構成された文字列は、辞書のキーなどとして検索に使用される可能性が高い、ということでこのような最適化をおこなっているようだ。