一覧へ

文字列の不変性とメモリの再割り当て

Pythonで文字列が不変である理由、メモリ内でどのように動作するか、パフォーマンスに与える影響について学びます。

中級
|
10
|
検証済み (2026-07)
文字列の不変性immutableメモリ割り当てインターニングid関数
進捗0/18 (0%)

文字列の不変性とメモリの再割り当て

このトピックを終えると

Pythonの文字列がなぜ不変(immutable)であるかを理解し、文字列操作がメモリ内でどのように動作するかを知り、大量の文字列処理におけるパフォーマンスの落とし穴を回避できるようになります。


文字列は修正できない

python
name = "Hello"
name[0] = "h"
# TypeError: 'str' object does not support item assignment

Pythonの文字列は不変(immutable)なオブジェクトです。一度作成されると、内容は変更できません。一見「修正」しているように見える操作は、実際には新しい文字列を作成します。

python
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}
python
# リスト: 可変 — 内容を直接修正可能
items = [1, 2, 3]
items[0] = 99
print(items) # [99, 2, 3] — 同じオブジェクト、内容のみ変更
# 文字列: 不変 — 修正不可、新しいオブジェクト作成
text = "hello"
new_text = text.replace("h", "H")
print(text) # "hello" — 元のまま
print(new_text) # "Hello" — 新しいオブジェクト

なぜ不変として設計したのか

1. ハッシュ可能 — 辞書のキーとして使用

python
# 文字列は辞書のキーとして使用可能(不変であるため)
scores = {"alice": 90, "bob": 85}
# リストは辞書のキーとして使用不可(可変であるため)
# scores = {[1, 2]: "value"} # TypeError: unhashable type: 'list'

辞書はキーのハッシュ値を使ってデータを検索します。キーが変更されるとハッシュ値も変わるため、データを検索できなくなります。不変オブジェクトのみハッシュ可能です。

2. 安全性 — 共有しても問題ない

python
def greet(name):
greeting = "Hello, " + name
return greeting
original = "World"
result = greet(original)
print(original) # "World" — 関数がoriginalを修正できない

関数に文字列を渡しても、関数内で元の文字列が変更される心配はありません。リストは関数内で.append()を使って元の文字列が修正される可能性があります。

3. スレッド安全性

複数のスレッドが同じ文字列を同時に読み込んでも、誰も変更できないため、ロック(lock)は必要ありません。


文字列インターニング

Pythonはよく使われる文字列を再利用します。これをインターニング(interning)と呼びます。

python
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つだけ存在します。これは不変であるため可能です — 共有しても誰かが修正することができません。

python
# is vs ==
a = "hello"
b = "hello"
print(a == b) # True — 値は同じか?
print(a is b) # True — 同じオブジェクトか?(インターニングのため)
# 文字列の比較は常に == を使用。is は予測不可能

パフォーマンスの落とし穴 — 文字列の連結

python
# 悪い: ループで文字列を連結 — O(n²)
result = ""
for i in range(10000):
result += str(i) + "," # 毎回新しい文字列を作成!

各反復で新しい文字列を作成し、以前の内容をコピーします。10,000回反復すると、約5千万回の文字列のコピーが発生します(1 + 2 + 3 + ... + 10000)。

python
# 良い: join を使用 — O(n)
result = ",".join(str(i) for i in range(10000))

join()は最終的なサイズを事前に計算し、メモリを1回だけ割り当てます。コピー回数がO(n)に減ります。

パフォーマンスの比較

python
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 io
start = 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

実践的なパターン

文字列を「修正」する方法

python
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 の組み合わせ

python
name = "Hoon"
age = 30
greeting = f"Hello, {name}! You are {age} years old."

f-string は内部的に一度に最終的な文字列を作成します。+を使って複数回連結するよりも効率的で、可読性も優れています。


他の言語との比較

java
// Java — String は不変、StringBuilder は可変
String s = "Hello";
s = s + " World";  // 新しい String オブジェクトを作成

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // 同じオブジェクト内で修正(可変)
String result = sb.toString();
javascript
// JavaScript — 文字列は不変(Python と同じ)
let s = "Hello";
s[0] = "h";      // エラーは発生しないが無視される
console.log(s);  // "Hello" (変更されない)
c
// 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() を使用すること

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...