【Python入門】ミュータブルとイミュータブルの違いをわかりやすく解説

はじめに

Pythonプログラミングを学習していると、「ミュータブル(mutable)」と「イミュータブル(immutable)」という用語に出会うことがあります。これらはPythonのオブジェクトの重要な特性であり、プログラムの動作を理解する上で欠かせない概念です。

本記事では、Python初心者の方に向けて、ミュータブルとイミュータブルの違いを基礎から丁寧に解説します。この違いを理解することで、予期しないバグを防ぎ、より効率的なコードを書けるようになります。

ミュータブルとイミュータブルとは?

基本的な定義

**ミュータブル(mutable)**とは、「変更可能」という意味です。ミュータブルなオブジェクトは、一度作成された後でも、その内容を変更することができます。オブジェクトのIDは変わらず、中身だけが書き換えられるイメージです。

一方、**イミュータブル(immutable)**は「変更不可能」を意味します。イミュータブルなオブジェクトは、作成後に内容を変更することができません。値を変更しようとすると、実際には新しいオブジェクトが作成されます。

なぜこの概念が重要なのか

ミュータブルとイミュータブルの違いを理解していないと、以下のような問題が発生する可能性があります。

  • 関数に渡した変数が意図せず変更されてしまう
  • 複数の変数が同じオブジェクトを参照していることに気づかない
  • パフォーマンスの問題が発生する
  • デバッグが困難になる

これらの問題を避けるためにも、それぞれの特性をしっかりと理解することが大切です。

Pythonにおけるミュータブルとイミュータブルの分類

イミュータブルなオブジェクト

Pythonでイミュータブルとされる主なデータ型は以下の通りです。

整数(int): 数値計算の基本となる型です。整数値は変更できず、計算結果は常に新しいオブジェクトとして生成されます。

浮動小数点数(float): 小数を扱う型で、整数と同様にイミュータブルです。

文字列(str): テキストデータを扱う型です。文字列は一度作成されると変更できません。文字列操作を行うと、常に新しい文字列オブジェクトが作成されます。

タプル(tuple): 複数の要素を格納できるコレクション型ですが、要素の追加・削除・変更ができません。

論理値(bool): TrueまたはFalseの2つの値のみを持つ型です。

None型: Pythonにおける「値が存在しない」ことを表す特別な型です。

ミュータブルなオブジェクト

Pythonでミュータブルとされる主なデータ型は以下の通りです。

リスト(list): 最も一般的なコレクション型で、要素の追加、削除、変更が自由に行えます。

辞書(dict): キーと値のペアを格納するデータ構造で、キーの追加・削除や値の変更が可能です。

集合(set): 重複のない要素の集まりで、要素の追加・削除ができます。

バイト配列(bytearray): バイナリデータを扱う際に使用され、内容を変更できます。

具体的な動作の違い

イミュータブルオブジェクトの動作例

文字列を例に見てみましょう。

text = "Hello"
print(id(text))  # オブジェクトのIDを表示

text = text + " World"
print(id(text))  # 新しいオブジェクトのIDを表示
このコードでは、最初のtextと、" World"を追加した後のtextは、異なるIDを持ちます。つまり、文字列の連結操作によって新しいオブジェクトが作成されているのです。元の"Hello"という文字列オブジェクト自体は変更されていません。

整数の場合も同様です。変数に新しい値を代入すると、実際には新しい整数オブジェクトが作成され、変数がそれを参照するようになります。

ミュータブルオブジェクトの動作例

リストの場合は動作が異なります。

numbers = [1, 2, 3]
print(id(numbers))  # オブジェクトのIDを表示

numbers.append(4)
print(id(numbers))  # 同じIDのまま
この例では、appendメソッドで要素を追加しても、リストオブジェクトのIDは変わりません。つまり、同じオブジェクトの中身が変更されているのです。

ミュータブルとイミュータブルによる影響

関数の引数として渡す場合

Pythonでは、関数に引数を渡す際に「参照渡し」が行われます。この時、ミュータブルオブジェクトとイミュータブルオブジェクトでは挙動が大きく異なります。

イミュータブルなオブジェクト(例:整数、文字列)を関数に渡した場合、関数内で値を変更しようとしても、新しいオブジェクトが作成されるため、元の変数には影響しません。これは実質的に「値渡し」のような動作になります。

一方、ミュータブルなオブジェクト(例:リスト、辞書)を関数に渡した場合、関数内での変更が元のオブジェクトにも反映されます。これは関数の外側と内側で同じオブジェクトを参照しているためです。

def modify_data(num, lst):
    num = num + 10  # 新しいオブジェクトが作成される
    lst.append(4)   # 元のオブジェクトが変更される

number = 5
my_list = [1, 2, 3]

modify_data(number, my_list)

print(number)    # 5(変更されていない)
print(my_list)   # [1, 2, 3, 4](変更されている)

代入とコピーの違い

ミュータブルオブジェクトを扱う際は、代入とコピーの違いに注意が必要です。

単純な代入では、新しい変数は元の変数と同じオブジェクトを参照します。これを「浅いコピー(shallow copy)」と呼ぶこともあります。

list1 = [1, 2, 3]
list2 = list1  # 同じオブジェクトを参照

list2.append(4)
print(list1)  # [1, 2, 3, 4](list1も変更される)
この例では、list2に要素を追加したにもかかわらず、list1も変更されています。これは両方の変数が同じリストオブジェクトを指しているためです。

実際に独立したコピーを作成したい場合は、copyメソッドやlist()関数を使用します。

list1 = [1, 2, 3]
list2 = list1.copy()  # 新しいオブジェクトを作成

list2.append(4)
print(list1)  # [1, 2, 3](変更されない)
print(list2)  # [1, 2, 3, 4]

デフォルト引数の落とし穴

ミュータブルオブジェクトを関数のデフォルト引数に指定すると、予期しない動作を引き起こすことがあります。

def add_item(item, items=[]):  # 注意:これは問題のあるコード
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2](期待と異なる結果)
この例では、2回目の関数呼び出しで[2]が返されることを期待するかもしれませんが、実際には[1, 2]が返されます。これは、デフォルト引数のリストが関数定義時に一度だけ作成され、その後の呼び出しで同じオブジェクトが再利用されるためです。

正しい書き方は以下の通りです。

def add_item(item, items=None):
    if items is None:
        items = []  # 毎回新しいリストを作成
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [2](期待通りの結果)

タプルの特殊なケース

タプルはイミュータブルですが、タプルに含まれる要素がミュータブルな場合、その要素は変更可能です。

my_tuple = (1, 2, [3, 4])
my_tuple[2].append(5)  # リスト部分は変更可能
print(my_tuple)  # (1, 2, [3, 4, 5])
タプル自体の構造(要素数や要素の参照先)は変更できませんが、ミュータブルな要素の内容は変更できるのです。これは初心者が混乱しやすいポイントなので、注意が必要です。

パフォーマンスへの影響

イミュータブルオブジェクトのメリット

イミュータブルオブジェクトは、変更されないことが保証されているため、以下のようなメリットがあります。

  • ハッシュ化が可能: 辞書のキーや集合の要素として使用できます
  • スレッドセーフ: マルチスレッド環境でも安全に使用できます
  • メモリ効率: 同じ値のオブジェクトを再利用できる場合があります

ミュータブルオブジェクトのメリット

ミュータブルオブジェクトは、以下のような場面で効率的です。

  • 頻繁な更新: 要素の追加や削除を繰り返す場合、新しいオブジェクトを作成する必要がありません
  • 大きなデータ構造: メモリのコピーを避けられるため、大きなデータを扱う際に効率的です
  • インプレース操作: 既存のオブジェクトを直接変更できるため、メモリ使用量を抑えられます

文字列連結のパフォーマンス問題

イミュータブルな文字列を大量に連結する場合、パフォーマンスの問題が発生することがあります。

# 非効率な方法
result = ""
for i in range(1000):
    result = result + str(i)  # 毎回新しい文字列が作成される
このコードでは、ループのたびに新しい文字列オブジェクトが作成されるため、非常に非効率です。このような場合は、リストを使って要素を集め、最後にjoinメソッドで連結する方が効率的です。

# 効率的な方法
parts = []
for i in range(1000):
    parts.append(str(i))
result = "".join(parts)  # 一度だけ連結

実践的なベストプラクティス

ミュータブルオブジェクトを扱う際の注意点

  1. 意図しない変更を防ぐ: 関数にミュータブルオブジェクトを渡す際は、コピーを渡すことを検討しましょう。特に、元のデータを保持する必要がある場合は重要です。

  2. デフォルト引数には使わない: 関数のデフォルト引数には、空のリストや辞書を直接指定せず、Noneを使用し、関数内で新しいオブジェクトを作成しましょう。

  3. 明示的なコピー: 独立したコピーが必要な場合は、copy()deepcopy()を使用して明示的にコピーを作成しましょう。

イミュータブルオブジェクトを活用する

  1. 辞書のキー: 辞書のキーには、イミュータブルなオブジェクトのみが使用できます。文字列、数値、タプル(イミュータブルな要素のみを含む)などを使いましょう。

  2. 定数として使用: 変更されるべきでない値は、イミュータブルなオブジェクトで表現すると安全です。

  3. 関数の戻り値: 関数が複数の値を返す際、タプルを使用すると、返された値が変更されないことが保証されます。

まとめ

ミュータブルとイミュータブルの違いは、Pythonプログラミングにおける基本的かつ重要な概念です。

**イミュータブル(変更不可能)**なオブジェクトには、整数、浮動小数点数、文字列、タプルなどがあり、値を変更しようとすると新しいオブジェクトが作成されます。これにより、データの一貫性が保たれ、予期しない変更を防ぐことができます。

**ミュータブル(変更可能)**なオブジェクトには、リスト、辞書、集合などがあり、オブジェクト自体を変更できます。これにより、効率的なデータ操作が可能になりますが、意図しない変更に注意が必要です。

この違いを理解することで、以下のようなメリットが得られます。

  • 関数の引数や戻り値を適切に扱える
  • 予期しないバグを防止できる
  • パフォーマンスを最適化できる
  • より保守性の高いコードを書ける

初めは複雑に感じるかもしれませんが、実際にコードを書きながら経験を積むことで、自然と理解が深まります。ミュータブルとイミュータブルの特性を意識しながらプログラミングを続けていきましょう。

「らくらくPython塾」が切り開く「呪文コーディング」とは?

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■初心者歓迎「AI駆動開発/生成AIエンジニアコース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

格安のプログラミングスクールといえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
対面型でより早くスキル獲得、月額2万円のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<オンライン無料>ゼロから始めるPython爆速講座