文字列とリストの`is`の謎: Pythonのインターン(Intering)・文字列インターン(String Intering)

こんにちは、ピリカ開発チームの冨田です。

Pythonにはインターン(Intern, Intering)という仕組みがあります。

ここでは、インターンとは何か、Pythonインターンが利用されるのはどの様な場合か、文字列がインターンされる条件は何かなど、Pythonインターンについて書いています。

概要

Pythonでは、文字列などのオブジェクトについてインターン(Intern, Intering)という仕組みが利用されています。

インターンとは、イミュータブルな値を1つだけ保存して再利用する仕組みのことです。

今回は、インターンとは何か、インターンが利用されるのはどの様な場合か、文字列でインターンが利用される場合どの様な挙動を取るのかについて、書いていこうと思います。

(背景) インターンについて調べようと思った背景

Pythonでは、変数の比較にis==があります。

isは、同一のオブジェクトかどうかを判定、すなわち識別値が一致しているかを判定します。

==は、等価のオブジェクトであるかどうかを判定します。

ただ、これらを利用した時リストと文字列で挙動が異なることに気づきました。

>>> str1 = "Hello"
>>> str2 = "Hello"
>>> print(str1 is str2)
True
>>> print(str1 == str2)
True
>>> list1 = [1, 2, 3]
>>> list2 = [1, 2, 3]
>>> print(list1 is list2)
False
>>> print(list1 == list2)
True

調べていると、その違いはインターンという仕組みに由来するということがわかり、インターンについて詳しく調べてみることにしました。

リストと文字列の謎

謎は、リストの識別値と文字列の識別値の挙動の違いでした。

id()は識別値を取得する関数です

詳しく言うと、

Q.変数を代入すると識別値は一致するか?

A.YES

これは自然と腑に落ちます。

>>> str1 = "Hello"
>>> str2 = str2
>>> print(str1 is str2)
True
>>> print(str1 == str2)
True
>>> print(id(str1), id(str2))
140516031370544 140516031370544

Q.それぞれ変数を宣言すると、識別値は一致するか? A.文字列はYES, リストはNO! 辞書もNO!

文字列とリストで挙動が異なることが疑問でした。

文字列は、それぞれ別に生成しても、値が同じであれば識別値が一致します。

>>> str1 = "Hello"
>>> str2 = "Hello"
>>> print(str1 is str2)
True
>>> print(str1 == str2)
True
>>> print(id(str1), id(str2))
140516031370544 140516031370544  # 識別値が一致する

リストは、値が同じであっても、別々に宣言すると識別値が異なります。

>>> list1 = [1, 2, 3]
>>> list2 = [1, 2, 3]
>>> print(list1 is list2)
False
>>> print(list1 == list2)
True
>>> print(id(list1),id(list2))
140515761167168 140515761204032  #  識別値が異なる

これは組み込み関数list()を利用しても一緒で、識別値が異なります。

>>> list1 = list([1, 2, 3])
>>> list2 = list([1, 2, 3])
>>> print(list1 is list2)
False
>>> print(list1 == list2)
True
>>> print(id(list1),id(list2))
140516031357088 140516031357168 # 識別値が異なる

また、辞書も識別値が異なります。

>>> dict1 = {"key", "value"}
>>> dict2 = {"key", "value"}
>>> print(dict1 == dict2)
True
>>> print(dict1 is dict2)
False
>>> print(id(dict1), id(dict2))
140516031390016 140516031390496 # 識別値が異なる

また、文字列であっても、宣言時に文字列同士を結合すれば一致しますが、変数作成して結合する(代入する)と、識別値が異なります。

>>> str1 = "Hello"
>>> str2 = "Hell" + "o"
>>> print(str1 == str2)
True
>>> print(str1 is str2)
True
>>> tem = "o"
>>> str3 = "Hell" + tem
>>> print(str1 == str3)
True
>>> print(str1 is str3)
False
>>> print(id(str1), id(str2), id(str3))
140516031370544 140516031370544 140516031371504 # 宣言時に結合したものは識別値が一致するが、代入したものは異なる

調べたところ、これらの謎を解く鍵はインターンという仕組みにありました。

(本題) Pythonインターン

Pythonインターンを理解するのにこちらの記事がとても参考になりました。

ここでは、この記事をベースに下記の3つのステップで説明していきます。

  1. インターンとは
  2. インターンが行われるオブジェクト
  3. インターンが行われるタイミング

1. インターン(intern)とは

インターンとは、新しいオブジェクトを生成する時に、全く新しいオブジェクトを生成するのではなく、再利用できる値がないかを参照する仕組みです。

もし既に利用されている値であれば、同じメモリを参照します。

まだ利用されていない値であれば、新しいメモリを利用します。

この、インターンを利用することで、メモリを節約したり、より早く動作させることができる様になります。

そのため、下記の様に、文字列が別々に宣言されても、値が同じであるために、同じ識別値が与えられます。

>>> str1 = "Hello"
>>> str2 = "Hello"  # 同じ値の"Hello"があるので同じ識別値が与えられている
>>> print(id(str1), id(str2))
140516031370544 140516031370544  # 識別値が一致する

これがインターンの基本です。 文字列のisがTrueになる理由はこれです。

2. インターンが行われるオブジェクト

インターンが行われるのは、イミュータブル(編集不可能)なオブジェクトに対してです。

Pythonにおいて、文字列は、イミュータブル(編集不可能)ですが、リストは、ミュータブル(修正可能)です。

そのため、文字列は宣言時に値が同じであれば、それぞれ同じ識別値を持ちましたが、リストは同じ値を持つリストであっても異なる識別値を持ちます。

(例) 文字列の場合

>>> str1 = "Hello"
>>> str2 = "Hello"  # インターンを利用
>>> print(id(str1), id(str2))
140516031370544 140516031370544  # 識別値が一致する

(例) リストの場合

>>> list1 = [1, 2, 3]
>>> list2 = [1, 2, 3]   # インターンを利用しない
>>> print(id(list1),id(list2))
140515761167168 140515761204032  #  識別値が異なる

文字列のisがTrueで、リストのisがFalseになる理由はこれです。

3. 文字列がインターン化されるタイミング

文字列がインターンされるかどうかには、いくつかの条件があります。

この、3つ目のコンパイル時というのが鍵で、メソッドを使って定義したり、変数を使って定義した文字列はファイル読み込み時には、コンピュートされず、インターン化もされません。

なので、変数を使って定義した文字列は、値が同じであっても識別値が異なります。

>>> str1 = "Hello"  # インターン化される
>>> str2 = "Hell" + "o"   # インターン化される
>>> tem = "o"
>>> str3 = "Hell" + tem  # 読み込み時にコンパイルできない。よってインターン化されていない
>>> print(id(str1), id(str2), id(str3))
140516031370544 140516031370544 140516031371504

同じように見える文字列であっても、宣言時と、変数利用時にisの挙動が異なるのはこのためでした。

強制的にインターン化させる方法

ここまでインターンについて説明してきました。

上記の様に、文字列はインターン化される場合とされない場合があります。

文字列を強制的にインターン化したい場合は、関数sys.intern()を利用することで実現可能です。

本来は、同じ識別値が与えられない文字列であっても、sys.intern()を使用することでインターン化し、値が同じであれば、同じ識別値を与えることができます。

>>> import sys
>>> str1 = "Hello"
>>> temp = "o"
>>> str2 = "Hell" + temp
>>> print(id(str1), id(str2), id(sys.intern(str2)))
140516031370544 140516031371888 140516031370544

最後に

私が一番最初に学んだ言語はPythonで、コンパイル型言語についてはCについて1冊本を読んだ程度です。

なので、普段コンパイルやメモリについて深く考えることはほとんどありません。

ただ、私がプログラミングを始めたばかりの頃、組み込みが得意だった友人が「Pythonってメモリが気になっちゃうんだよね」と言っていました。

私もやっとここを疑問に思うことができるようになったのかと、嬉しくなりました!

余談も多かったですが、ここまで読んでくださりありがとうございました。

参考