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

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

この記事ではPythonにおけるミュータブルとイミュータブルについて説明します。

YouTube Video

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

「ミュータブル」と「イミュータブル」はオブジェクトの「変更可能性」に関する性質です。これを理解しておくことで、予期せぬバグの回避効率的なメモリ管理に繋がります。

ミュータブルとは何か?

ミュータブル(mutable)なオブジェクトは、作成後にその内部の状態を変更することができるものです。

主なミュータブルなデータ型

  • list
  • dict
  • set
  • ユーザー定義のクラス(属性が変更可能な場合)

例:リストの変更

1numbers = [1, 2, 3]
2numbers[0] = 100
3print(numbers)  # [100, 2, 3]

リストはミュータブルなオブジェクトであり、要素を自由に変更できます。

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

**イミュータブル(immutable)**なオブジェクトは、一度作成すると変更できないものです。変更しようとすると、新しいオブジェクトが生成されます。

主なイミュータブルなデータ型

  • int
  • float
  • str
  • tuple
  • bool
  • frozenset

例:文字列の変更

1text = "hello"
2# text[0] = "H"  # TypeError: 'str' object does not support item assignment
3
4text = "H" + text[1:]  # Creates a new string
5print(text)  # "Hello"

文字列はイミュータブルなので、部分的に変更することはできません。

ミュータブルとイミュータブルの違いを比較

 1# Mutable example
 2a = [1, 2, 3]
 3b = a
 4b[0] = 100
 5print(a)  # [100, 2, 3] -> a is also changed
 6
 7# Immutable example
 8x = 10
 9y = x
10y = 20
11print(x)  # 10 -> x is unchanged

この例から分かるように、ミュータブルなオブジェクトは参照によって共有されるため、他の変数にも影響を及ぼす可能性があります。一方、イミュータブルなオブジェクトは再代入により新しいオブジェクトが作られ、元の値には影響しません

id() を使って内部の挙動を確認する

Pythonでは、id()関数でオブジェクトのIDを確認できます。ここで、オブジェクトのIDは、メモリアドレスのようなものです。

 1# Immutable int behavior
 2a = 10
 3print(id(a))  # e.g., 140715920176592
 4a += 1
 5print(id(a))  # e.g., 140715920176624 -> ID has changed
 6
 7# Mutable list behavior
 8b = [1, 2, 3]
 9print(id(b))  # e.g., 2819127951552
10b.append(4)
11print(id(b))  # Same ID -> only the content has changed

このように、イミュータブルでは新しいオブジェクトが生成され、ミュータブルでは同じオブジェクトに変更が加えられます。

関数とミュータブル・イミュータブルの注意点

関数にミュータブルなオブジェクトを渡すと元のデータが変更される可能性があります。

例:リストを変更する関数

1def modify_list(lst):
2    lst.append(100)
3
4my_list = [1, 2, 3]
5modify_list(my_list)
6print(my_list)  # [1, 2, 3, 100]

例:数値を変更する関数

一方、イミュータブルなオブジェクトを変更しようとした場合は新しいオブジェクトが作成されます。

1def modify_number(n):
2    n += 10
3
4my_number = 5
5modify_number(my_number)
6print(my_number)  # 5 -> unchanged

実践的な注意点

デフォルト引数にミュータブルなオブジェクトを使わない

 1# Bad example
 2def add_item(item, container=[]):
 3    container.append(item)
 4    return container
 5
 6print(add_item(1))  # [1]
 7print(add_item(2))  # [1, 2] -> unintended behavior
 8
 9# Good example
10def add_item(item, container=None):
11    if container is None:
12        container = []
13    container.append(item)
14    return container
15
16print(add_item(1))  # [1]
17print(add_item(2))  # [2]

デフォルト引数は関数定義時に一度だけ評価されるため、ミュータブルなオブジェクトを使うと副作用が発生します。

  • 最初の例では、add_item を呼び出すたびに同じリストオブジェクトが使われます。2回目の add_item(2) のとき、最初に追加した1がすでに入っていて、結果が [1, 2] になります。
  • 改善例では、デフォルト値に None を使い、関数内で None だった場合に新しくリストを作成するようにしています。これにより、関数を呼び出すたびに新しいリストが作られるため、前の呼び出し結果が影響しません。

デフォルト引数にリストや辞書などのミュータブルなオブジェクトを使うのを避け、代わりに None を使って関数内で初期化します。これはPythonにおける基本的かつ重要なベストプラクティスです。

まとめ

Pythonの変数やデータ型を深く理解するには、「ミュータブルとイミュータブルの違い」を意識することがとても重要です。これらの特性を理解していれば、コードの意図しない挙動を防ぎ、より堅牢で読みやすいプログラムを書くことができます。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video