概要
Pythonでは、文字列などのオブジェクトについてインターン(Intern, Intering)という仕組みが利用されています。
インターンとは、イミュータブルな値を1つだけ保存して再利用する仕組みのことです。
今回は、インターンとは何か、インターンが利用されるのはどの様な場合か、文字列でインターンが利用される場合どの様な挙動を取るのかについて、書いていこうと思います。
(背景) インターンについて調べようと思った背景
Pythonでは、変数の比較にis
と==
があります。
is
は、同一のオブジェクトかどうかを判定、すなわち識別値が一致しているかを判定します。
==
は、等価のオブジェクトであるかどうかを判定します。
いつもis
と==
の使い分けに迷うので、そもそもどういった場合に同じ識別値が与えられ、どういった場合に異なる識別値となるのかを調べていました。
そこで、リストと文字列で挙動が異なることに気づきました。
調べていると、その違いはインターンという仕組みに由来するということがわかり、インターンについて詳しく調べてみることにしました。
リストと文字列の謎
疑問だったのは、リストの識別値と文字列の識別値の挙動の違いでした。
詳しく言うと、
Q. 変数を代入すると識別値は一致するか?
→ A. YES。
これは自然と腑に落ちます。
>>> str1 = "Hello"
>>> str2 = str2
>>> print(id(str1), id(str2))
140516031370544 140516031370544
Q. それぞれ変数を宣言すると、識別値は一致するか?
→ A. 文字列はYES, リストはNO?!
文字列とリストで挙動が異なることが疑問でした。
文字列は、それぞれ別に生成しても、値が同じであれば識別値が一致します。
>>> 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 # 識別値が異なる
これは組み込み関数list()
を利用しても一緒で、識別値が異なります。
>>> list1 = list([1, 2, 3])
>>> list2 = list([1, 2, 3])
>>> print(id(list1),id(list2))
140516031357088 140516031357168
また、辞書も識別値が異なります。
>>> dict1 = {"key", "value"}
>>> dict2 = {"key", "value"}
>>> print(id(dict1), id(dict2))
140516031390016 140516031390496
また、文字列であっても、宣言時に文字列同士を結合すれば一致しますが、変数を結合する(代入する)と、識別値が異なります。
>>> str1 = "Hello"
>>> str2 = "Hell" + "o"
>>> tem = "o"
>>> str3 = "Hell" + tem
>>> print(id(str1), id(str2), id(str3))
140516031370544 140516031370544 140516031371504
これらの謎を解く鍵はインターンという仕組みにあります。
(本題) Pythonのインターン
Pythonのインターンを理解するのにこちらの記事がとても参考になりました。
ここでは、この記事をベースに下記の3つのステップで説明していきます。
- インターンとは
- インターンが行われるオブジェクト
- インターンが行われるタイミング
1. インターン(intern)とは
インターンとは、新しいオブジェクトを生成する時に、全く新しいオブジェクトを生成するのではなく、再利用できる値がないかを参照する仕組みです。
もし既に利用されている値であれば、同じメモリを参照します。
まだ利用されていない値であれば、新しいメモリを利用します。
この、インターンを利用することで、メモリを節約したり、より早く動作させることができる様になります。
そのため、下記の様に、文字列が別々に宣言されても、値が同じであるために、同じ識別値が与えられます。
>>> str1 = "Hello"
>>> str2 = "Hello" # 同じ値の"Hello"があるので同じ識別値が与えられている
>>> print(id(str1), id(str2))
140516031370544 140516031370544 # 識別値が一致する
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 # 識別値が異なる
3. 文字列のインターン
ただし、文字列がインターンされるかどうかには、いくつかの条件があります。
-
全ての空文字列と長さが1の文字列はインターン化される。
-
2文字以上の文字列は、Pythonのバージョンによる。
- バージョン3.7以前は、20文字未満の文字列はインターン化される。
- それ以上は、現在は4096文字までインターン化される。
-
文字列は、コンパイル時にのみインターン化される。そのため、コンパイル時にコンピュートできない値はインターン化されない。
この、3つ目のコンパイル時というのが鍵で、メソッドを使って定義したり、変数を使って定義した文字列はファイル読み込み時には、コンピュートされず、インターン化もされません。
なので、変数を使って定義した文字列は、値が同じであっても識別値が異なります。
>>> str1 = "Hello" # インターン化される
>>> str2 = "Hell" + "o" # インターン化される
>>> tem = "o"
>>> str3 = "Hell" + tem # 読み込み時にコンパイルできない。よってインターン化されていない
>>> print(id(str1), id(str2), id(str3))
140516031370544 140516031370544 140516031371504
強制的にインターン化させる方法
ここまでインターンについて説明してきました。
上記の様に、文字列はインターン化される場合とされない場合があります。
文字列を強制的にインターン化したい場合は、関数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ってメモリが気になっちゃうんだよね」と言っていました。
私もやっとここを疑問に思うことができるようになったのかと、嬉しくなりました!
余談も多かったですが、ここまで読んでくださりありがとうございました。