文字列の不変性とメモリの再割り当て
このトピックを終えると
Pythonの文字列がなぜ不変(immutable)であるかを理解し、文字列操作がメモリ内でどのように動作するかを知り、大量の文字列処理におけるパフォーマンスの落とし穴を回避できるようになります。
文字列は修正できない
name = "Hello"name[0] = "h"# TypeError: 'str' object does not support item assignmentPythonの文字列は不変(immutable)なオブジェクトです。一度作成されると、内容は変更できません。一見「修正」しているように見える操作は、実際には新しい文字列を作成します。
name = "Hello"print(id(name)) # 4385123456
name = name.lower()print(id(name)) # 4385123520 ← 別のオブジェクト!id()はオブジェクトのメモリアドレスを返します。lower()の後にidが変わっているので、既存の文字列を修正したのではなく、新しい文字列を作成したことになります。
可変 vs 不変
| 型 | 変更可能 | 例 |
|---|---|---|
str | 不変 | "hello" |
tuple | 不変 | (1, 2, 3) |
int | 不変 | 42 |
float | 不変 | 3.14 |
list | 可変 | [1, 2, 3] |
dict | 可変 | {"a": 1} |
set | 可変 | {1, 2, 3} |
# リスト: 可変 — 内容を直接修正可能items = [1, 2, 3]items[0] = 99print(items) # [99, 2, 3] — 同じオブジェクト、内容のみ変更
# 文字列: 不変 — 修正不可、新しいオブジェクト作成text = "hello"new_text = text.replace("h", "H")print(text) # "hello" — 元のままprint(new_text) # "Hello" — 新しいオブジェクトなぜ不変として設計したのか
1. ハッシュ可能 — 辞書のキーとして使用
# 文字列は辞書のキーとして使用可能(不変であるため)scores = {"alice": 90, "bob": 85}
# リストは辞書のキーとして使用不可(可変であるため)# scores = {[1, 2]: "value"} # TypeError: unhashable type: 'list'辞書はキーのハッシュ値を使ってデータを検索します。キーが変更されるとハッシュ値も変わるため、データを検索できなくなります。不変オブジェクトのみハッシュ可能です。
2. 安全性 — 共有しても問題ない
def greet(name): greeting = "Hello, " + name return greeting
original = "World"result = greet(original)print(original) # "World" — 関数がoriginalを修正できない関数に文字列を渡しても、関数内で元の文字列が変更される心配はありません。リストは関数内で.append()を使って元の文字列が修正される可能性があります。
3. スレッド安全性
複数のスレッドが同じ文字列を同時に読み込んでも、誰も変更できないため、ロック(lock)は必要ありません。
文字列インターニング
Pythonはよく使われる文字列を再利用します。これをインターニング(interning)と呼びます。
a = "hello"b = "hello"print(a is b) # True — 同じオブジェクトを共有!print(id(a) == id(b)) # True
c = "hello world!"d = "hello world!"print(c is d) # False or True — 実装によって異なる短くて単純な文字列(識別子のような形式)はインターニングされます。同じ内容の文字列が複数回登場しても、メモリには1つだけ存在します。これは不変であるため可能です — 共有しても誰かが修正することができません。
# is vs ==a = "hello"b = "hello"print(a == b) # True — 値は同じか?print(a is b) # True — 同じオブジェクトか?(インターニングのため)
# 文字列の比較は常に == を使用。is は予測不可能パフォーマンスの落とし穴 — 文字列の連結
# 悪い: ループで文字列を連結 — O(n²)result = ""for i in range(10000): result += str(i) + "," # 毎回新しい文字列を作成!各反復で新しい文字列を作成し、以前の内容をコピーします。10,000回反復すると、約5千万回の文字列のコピーが発生します(1 + 2 + 3 + ... + 10000)。
# 良い: join を使用 — O(n)result = ",".join(str(i) for i in range(10000))join()は最終的なサイズを事前に計算し、メモリを1回だけ割り当てます。コピー回数がO(n)に減ります。
パフォーマンスの比較
import time
# 方法 1: += (遅い)start = time.time()result = ""for i in range(100000): result += str(i)print(f"+=: {time.time() - start:.3f}s")
# 方法 2: join (速い)start = time.time()result = "".join(str(i) for i in range(100000))print(f"join: {time.time() - start:.3f}s")
# 方法 3: io.StringIO (大量のテキスト)import iostart = time.time()buf = io.StringIO()for i in range(100000): buf.write(str(i))result = buf.getvalue()print(f"StringIO: {time.time() - start:.3f}s")
# 結果の例:# +=: 0.852s# join: 0.031s (27倍速い)# StringIO: 0.029s実践的なパターン
文字列を「修正」する方法
text = "Hello, World!"
# 大文字小文字text.upper() # "HELLO, WORLD!"text.lower() # "hello, world!"text.title() # "Hello, World!"
# 置換text.replace("World", "Python") # "Hello, Python!"
# 文字単位での修正(リスト経由)chars = list(text) # ['H', 'e', 'l', 'l', 'o', ...]chars[0] = 'h'result = "".join(chars) # "hello, World!"すべてのメソッドは新しい文字列を返します。元の文字列は変更されません。
f-string の組み合わせ
name = "Hoon"age = 30greeting = f"Hello, {name}! You are {age} years old."f-string は内部的に一度に最終的な文字列を作成します。+を使って複数回連結するよりも効率的で、可読性も優れています。
他の言語との比較
// Java — String は不変、StringBuilder は可変
String s = "Hello";
s = s + " World"; // 新しい String オブジェクトを作成
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 同じオブジェクト内で修正(可変)
String result = sb.toString();// JavaScript — 文字列は不変(Python と同じ)
let s = "Hello";
s[0] = "h"; // エラーは発生しないが無視される
console.log(s); // "Hello" (変更されない)// C — char 配列は可変
char s[] = "Hello";
s[0] = 'h'; // OK — "hello" に変更されるほとんどの現代的な言語(Python、Java、JavaScript、Go、C#)では、文字列は不変です。これは安全性と最適化のための共通の設計上の決定です。Java の StringBuilder、Go の strings.Builder は、Python の join() や io.StringIO と同様の役割を果たします — 可変バッファで組み立てた後、最終的な不変の文字列を作成するパターンです。
重要なまとめ
| 概念 | まとめ |
|---|---|
| 不変(immutable) | 作成後に内容を変更できない。すべての「修正」は新しいオブジェクトを作成 |
id() | オブジェクトのメモリアドレスを確認 |
| インターニング | 同じ文字列はメモリで再利用(不変であるため可能) |
join() | 文字列連結の正しい方法。+= ループは O(n²) |
is vs == | is はオブジェクトの同一性、== は値の等価性。文字列は == を使用 |
文字列の不変性は Python の設計上の決定です。辞書のキーとして使用可能、関数間での安全な伝達、インターニングによるメモリの最適化 — これらの利点が、「変更できない」という制約を上回ります。実践で覚えておくべきことは 1 つです。ループで += を使って文字列を積み重ねず、join() を使用すること。