Pythonのインターニング(Interning)と文字列インターン(String Interning)について、詳しく解説
Pythonを使い始めてしばらく経つと、ふとした瞬間に不思議な挙動に出会うことがあります。 例えば、同じ値を持つ2つの変数を用意したとき、それらがメモリ上の全く同じ場所を指していることもあれば、別の場所を指していることもあるのです。
この「裏側でこっそり行われているメモリ節約術」の正体こそが、インターニング(Interning)と呼ばれる仕組みです。 初心者の方にとっては少しマニアックな話に聞こえるかもしれませんが、これを知ることでPythonのメモリ管理への理解が飛躍的に深まります。
エンジニア歴10年の私も、駆け出しの頃はこの挙動に何度も頭を悩ませました。 なぜ特定の数字だけが同じオブジェクトとして扱われるのか、その理由を解き明かすことで、より効率的でミスのないコードが書けるようになります。
今回は、整数と文字列の2つの視点から、Pythonのインターニングを詳しく解説します。
インターニングとは?メモリを節約する賢い仕組み¶
まず最初に、インターニングという言葉の定義から確認していきましょう。 簡単に言うと、これは「同じ値を持つオブジェクトを、メモリ上に一つだけ作って共有する」という最適化の手法を指します。
通常、新しい変数に値を代入すると、コンピュータはその値を保存するための新しい領域をメモリ上に確保します。 しかし、よく使われる値(例えば 0 や 1 など)を毎回新しく作っていては、メモリがいくらあっても足りなくなってしまいますよね。
そこでPythonは、あらかじめ頻繁に使われるオブジェクトをメモリの特定の場所に用意しておき、それを使わせるように仕向けています。 これがインターニングの基本的な考え方です。
なぜこの仕組みが必要なのか¶
もしインターニングがなかったら、あなたのプログラムが 100回「10」という数値を使えば、メモリ上には100個の「10」という物体が存在することになります。 一つ一つは小さくても、積み重なれば膨大な無駄に繋がります。
インターニングによってオブジェクトを共有すれば、メモリ消費を大幅に抑えることができます。 さらに、同じオブジェクトを指しているかどうかをチェックするだけで値の比較が済むため、処理速度の向上にも寄与しているのです。
Pythonの「アイデンティティ」と「値」¶
ここで思い出してほしいのが、以前の記事でも触れた「変数の正体はラベルである」という考え方です。 インターニングが起きている状態とは、複数のラベル(変数名)がメモリ上のたった一つの実体を指している状態を指します。
この背景を理解しておくと、これからお話しする整数と文字列の具体的な挙動がスッと頭に入ってくるはずです。
【関連記事】Pythonの変数は箱じゃない?メモリ上のラベルという正体を理解する
整数のインターニング:-5から256の魔法¶
Pythonには、プログラムが起動した瞬間にあらかじめ作られる整数のセットがあります。 これをスモール・インテジャー・キャッシュ(Small Integer Cache)と呼びます。
この仕組みがあるおかげで、私たちは意識することなく高速な数値処理の恩恵を受けています。 しかし、このキャッシュには明確な「境界線」が存在することをご存知でしょうか。
境界線は「-5」から「256」¶
Python(正確には標準的なCPython)では、-5から256までの整数が自動的にインターニングされます。 この範囲の数字であれば、どこで何度使おうとも、メモリ上の全く同じオブジェクトが使い回されます。
a = 256
b = 256
print(a is b) # True になる(インターニングされている)
x = 257
y = 257
print(x is y) # False になる(範囲外なので別々に作られる)
なぜこの範囲なのかというと、これらがプログラムの中で最も頻繁に使われる数字だからです。 ループのカウンターやインデックスなど、日常的に登場する数字を優先的に効率化しているわけですね。
現場で陥る「is」と「==」の罠¶
この整数のインターニングを知らないと、数値の比較で大きなバグを埋め込んでしまう可能性があります。
値を比較するための == と、同一性を確認するための is を混同してしまうパターンです。
エンジニアとして10年働いていても、コードレビューでこのミスを見かけることがたまにあります。 「たまたま256以下の数字でテストしたときは動いていたのに、大きな数字を入れたら動かなくなった」という事態は、まさにこのインターニングが原因です。
数値の大きさに依存するような不安定なコードを書かないためにも、値の比較には常に == を使うことを徹底しましょう。
文字列インターニング:不変(イミュータブル)の恩恵¶
数値だけでなく、文字列もインターニングの対象となります。 これを特に文字列インターニング(String Interning)と呼びます。
文字列はPythonにおいて不変(イミュータブル)なオブジェクトであるため、複数の場所で共有しても中身を書き換えられる心配がありません。 この性質を利用して、Pythonは巧みにメモリを管理しています。
暗黙的なインターニング¶
Pythonは、特定の条件を満たす文字列を自動的にインターニングします。 例えば、ソースコードの中に直接書かれた短い文字列や、識別子(変数名や関数名)として使えそうな形式の文字列などが対象です。
s1 = "hello"
s2 = "hello"
print(s1 is s2) # True になることが多い
ただし、この自動的なインターニングがいつ、どの範囲で行われるかはPythonのバージョンや実装に依存します。 「必ずこうなる」という保証がないため、開発者がこれに頼りすぎるのは危険です。
動的に作られた文字列の挙動¶
一方で、実行中にユーザーから入力された文字列や、加工して作られた文字列は、通常は自動的にインターニングされません。 中身が同じであっても、Pythonは安全のためにそれぞれ別のオブジェクトとしてメモリを確保します。
s1 = "hello world"
s2 = "".join(["hello", " ", "world"])
print(s1 == s2) # True(値は同じ)
print(s1 is s2) # False(別々のオブジェクト)
このように、文字列の作り方によってインターニングの有無が変わるという点は、初心者が最も混乱しやすいポイントの一つです。
【関連記事】Pythonのisと== は何が違う? Pythonの比較演算子で知っておくべき罠
明示的なインターニング:sys.intern() の活用¶
基本的にはPythonにお任せで良いインターニングですが、私たち開発者が「これをインターニングして!」と明示的に指示することも可能です。
これには、標準ライブラリの sys.intern() という関数を使います。
大量の同じ文字列を扱うプログラムを書く際、この関数を知っているとメモリ消費を劇的に抑えることができるかもしれません。
sys.intern() の使い方¶
使い方は非常に簡単です。
インターニングしたい文字列を sys.intern() の引数に渡すだけです。
import sys
a = sys.intern("very_long_string_that_needs_interning")
b = sys.intern("very_long_string_that_needs_interning")
print(a is b) # 必ず True になる
これを行うと、Pythonは「インターニング済み文字列の辞書」を確認し、もし存在すればその既存のオブジェクトを返します。 なければ新しく登録し、以降は同じオブジェクトを使い回すようになります。
どんな時に使うべきか¶
正直なところ、日常的なアプリケーション開発で sys.intern() を使う機会はそれほど多くありません。
しかし、巨大なテキストデータを解析する場合や、特定のキーワードが数百万回登場するような場合には絶大な効果を発揮します。
メモリの節約だけでなく、文字列同士の比較が「ポインタ(メモリ上の住所)の比較」だけで済むようになるため、処理速度の高速化も期待できます。 プロのエンジニアは、パフォーマンスのボトルネックが文字列比較にあると判断した際に、このカードを切り札として使います。
インターニングの仕組み比較表¶
ここで、整数と文字列のインターニングの違いを一覧表で整理してみましょう。 それぞれの性質を正しく理解することで、使い分けが明確になります。
| 項目 | 整数のインターニング | 文字列のインターニング |
|---|---|---|
| 対象の範囲 | -5 から 256 まで | 条件を満たす識別子など |
| タイミング | インタープリタ起動時に固定 | 読み込み時や実行時 |
| 手動設定 | 不可能(固定されている) | sys.intern() で可能 |
| 主な目的 | 頻出する数値の計算高速化 | メモリ節約と辞書キーの高速検索 |
| 注意点 | 範囲外は同一性が保証されない | 動的な生成では共有されない |
このように、数値は「頻度」に基づいて自動化されており、文字列は「不変性」を活かした柔軟な管理が行われていることがわかりますね。
インターニングが救ったトラブル¶
私が経験したプロジェクトで、この知識が役立った具体的なエピソードをお話しします。 ある時、大量の自然言語データを処理するバッチプログラムが、メモリ不足(Out of Memory)で頻繁に停止していました。
原因を調査すると、数十万行のデータに含まれる「カテゴリー名」が、それぞれ独立したオブジェクトとしてメモリを圧迫していたのです。 カテゴリーの種類自体は数百種類しかないにもかかわらず、です。
解決策はたった一行の修正¶
そこで、データの読み込み部分に sys.intern() を導入しました。
カテゴリー名を読み込む際に、インターニングを介して共通のオブジェクトを指すように修正したのです。
# 修正前
category = row['category_name']
# 修正後
import sys
category = sys.intern(row['category_name'])
結果として、メモリ使用量は 40% 以上削減され、プログラムの実行速度も向上しました。 文字列比較を多用するロジックだったため、インターニングによる恩恵が最大化した好例です。
こうした「裏側の仕組み」を知っているだけで、小手先のテクニックではなく、本質的な改善ができるようになります。
【関連記事】 __slots__でメモリ使用量を劇的に削減する裏技テクニック
初学者が気をつけるべき「インターニングとの付き合い方」¶
ここまでインターニングのメリットを強調してきましたが、最後に注意点をお伝えします。 インターニングはあくまでPythonの「最適化」の一つであり、言語仕様の根幹ではありません。
挙動に依存したコードを書かない¶
最も大切なのは、インターニングの挙動を前提としたロジックを組まないことです。 「この数字は必ず同じオブジェクトになるはずだ」という期待は、将来のPythonのアップデートや異なる環境で裏切られる可能性があります。
値を比較したいときは、常に == を使ってください。
is を使って良いのは、None との比較や、意図的に同一性をチェックしたい特別な場合に限られます。
メモリを意識しすぎないことも大切¶
現代のコンピュータはメモリが豊富です。
初心者のうちからすべての文字列を sys.intern() しようとするのは、いわゆる「早すぎる最適化」に当たります。
コードの可読性を犠牲にしてまで最適化を行うのは、本末転倒です。 まずは読みやすく、正しいコードを書くことを優先し、インターニングは「困った時の解決策」として知識の引き出しにしまっておきましょう。
まとめ¶
Pythonのインターニングは、一見すると地味ですが、言語のパフォーマンスを支える非常に知的な仕組みです。 私たちが快適にコードを書けている裏側で、Pythonがせっせとメモリを節約してくれている様子を想像すると、少し愛着が湧いてきませんか?
今回学習した内容をまとめます。
- インターニングは、同じ実体を共有してメモリと時間を節約する手法。
- 整数のキャッシュ範囲(-5〜256)を意識しつつ、比較は
==を使う。 - 文字列は不変性を活かして効率的に管理されている。
sys.intern()は、大量の重複データを扱う際の強力な武器になる。
こうした「マニアックだけど重要な知識」を一つずつ積み重ねていくことで、あなたは初心者から一歩抜け出したエンジニアへと成長していきます。 仕組みを知ることは、単にバグを減らすだけでなく、プログラミングそのものを深く楽しむことに繋がります。
これからも、Pythonの奥深い仕組みを一緒に探求していきましょう。
ここまでお読みいただきありがとうございました。