Pythonの参照渡しとは?イミュータブルとミュータブルの違いとその活用法

1. 値渡しと参照渡しの違い

Pythonにおいて、関数に引数を渡す方法には「値渡し」と「参照渡し」があります。

  • 値渡し:関数に引数として値のコピーを渡す方法で、関数内で引数を変更しても、元の変数には影響を与えません。
  • 参照渡し:変数の参照(アドレス)を関数に渡す方法で、関数内での変更が元の変数にも反映されます。

Pythonでは、オブジェクトの性質に応じてこの挙動が異なります。Pythonの「参照渡し」は、特にミュータブルなデータ型に適用され、コードの動作に大きな影響を与えるため、正しく理解することが重要です。

2. Pythonにおけるオブジェクトの特性

Pythonでは、すべてのデータがオブジェクトとして扱われ、オブジェクトの特性により、イミュータブル(変更不可)ミュータブル(変更可能)のどちらかに分類されます。この特性により、関数に引数として渡されたときの動作が異なります。

  • イミュータブルオブジェクト:生成後に変更ができないオブジェクト。例として、整数(int)、浮動小数点数(float)、文字列(str)、タプル(tuple)などが挙げられます。これらのデータ型は一度作成されると変更できないため、参照渡しであっても関数内の操作が外部に影響を与えません。
  • ミュータブルオブジェクト:生成後も変更が可能なオブジェクト。例として、リスト(list)、辞書(dict)、集合(set)などが挙げられます。これらのデータ型は関数内で変更するとその変更が呼び出し元にも反映されるため、Pythonの参照渡しの影響を特に受けます。

3. Pythonの引数渡しの仕組み

Pythonでは、関数に引数を渡す際、オブジェクトへの参照が渡されます。この挙動は「参照の値渡し」と呼ばれることもあります。ここからは、イミュータブルとミュータブルの違いによるPython特有の動作を具体的な例で解説します。

3.1 イミュータブルオブジェクトの場合

イミュータブルオブジェクトを関数に渡すと、関数内で新たな値を代入しようとすると新しいオブジェクトが生成され、元のオブジェクトには影響を与えません。

例:イミュータブルな整数の引数渡し

def modify_number(num):
    num = num * 2
    print("関数内の数値:", num)

original_num = 5
modify_number(original_num)
print("関数外の数値:", original_num)

出力:

関数内の数値: 10
関数外の数値: 5

解説
関数modify_number内で変数numを2倍にしていますが、関数外のoriginal_numには影響がありません。整数がイミュータブルであるため、新たな値が代入される際に別のオブジェクトが生成され、original_numには変更が加えられないのです。

3.2 ミュータブルオブジェクトの場合

一方、ミュータブルオブジェクトを関数に渡すと、その参照が関数内でも利用されるため、関数内での変更が元のオブジェクトにも反映されます。

例:ミュータブルなリストの引数渡し

def add_item(my_list):
    my_list.append("追加された要素")
    print("関数内のリスト:", my_list)

original_list = ["要素1", "要素2"]
add_item(original_list)
print("関数外のリスト:", original_list)

出力:

関数内のリスト: ['要素1', '要素2', '追加された要素']
関数外のリスト: ['要素1', '要素2', '追加された要素']

解説
リストがミュータブルであるため、関数add_item内での変更がそのまま関数外のoriginal_listにも反映されています。このように、ミュータブルなオブジェクトは、関数内外で共有されるため、関数内での変更が呼び出し元にも影響を与えます。

4. 参照渡しに関する注意点と対策

Pythonでミュータブルオブジェクトを関数に渡す場合、予期せぬ動作を引き起こす可能性があります。このような問題を回避するための対策を紹介します。

4.1 オブジェクトのコピーを使用する

関数内でオブジェクトを変更したい場合、元のオブジェクトをコピーしてから操作することで、元のオブジェクトへの影響を防ぐことができます。Pythonのcopyモジュールを使用して浅いコピー(copy.copy)や深いコピー(copy.deepcopy)を作成できます。

例:深いコピーを用いたミュータブルオブジェクトの回避策

import copy

def add_item(my_list):
    my_list_copy = copy.deepcopy(my_list)
    my_list_copy.append("追加された要素")
    print("関数内のリスト(コピー):", my_list_copy)

original_list = ["要素1", "要素2"]
add_item(original_list)
print("関数外のリスト:", original_list)

出力:

関数内のリスト(コピー): ['要素1', '要素2', '追加された要素']
関数外のリスト: ['要素1', '要素2']

解説
deepcopyを用いることで関数内で新しいオブジェクトが生成され、元のリストには影響がありません。これにより、関数内外で独立した変更が可能になります。

4.2 デフォルト引数にNoneを使用する

関数のデフォルト引数としてミュータブルオブジェクトを設定するのは避け、デフォルト引数にNoneを使用し、関数内で新たにオブジェクトを作成する方法が推奨されます。

例:デフォルト引数をNoneにする安全な書き方

def add_item(my_list=None):
    if my_list is None:
        my_list = []
    my_list.append("新しい要素")
    print("関数内のリスト:", my_list)
    return my_list

# リストを渡さない場合
result = add_item()
print("返されたリスト:", result)

# リストを渡す場合
existing_list = ["要素1", "要素2"]
result = add_item(existing_list)
print("返されたリスト:", result)
print("元のリスト:", existing_list)

出力:

関数内のリスト: ['新しい要素']
返されたリスト: ['新しい要素']
関数内のリスト: ['要素1', '要素2', '新しい要素']
返されたリスト: ['要素1', '要素2', '新しい要素']
元のリスト: ['要素1', '要素2', '新しい要素']

解説
デフォルト引数Noneを使用し、関数内で新しいリストを生成することで、関数外のリストが予期せず変更されるのを防ぐことができます。Noneをデフォルト引数として設定することで、新しいリストを作成するか、指定されたリストを使用するかが明確に分かります。

5. 実践例:参照渡しの理解を深めるコード例

ここまでの内容を実践的な例を通して確認しましょう。

辞書のキーと値を変更する例

辞書もミュータブルオブジェクトであり、関数内での操作がそのまま関数外にも影響します。以下の例では、辞書のキーと値を変更する際の動作を確認します。

def modify_dict(my_dict):
    my_dict["新しいキー"] = "新しい値"
    print("関数内の辞書:", my_dict)

original_dict = {"キー1": "値1", "キー2": "値2"}
modify_dict(original_dict)
print("関数外の辞書:", original_dict)

出力:

関数内の辞書: {'キー1': '値1', 'キー2': '値2', '新しいキー': '新しい値'}
関数外の辞書: {'キー1': '値1', 'キー2': '値2', '新しいキー': '新しい値'}

解説
辞書がミュータブルなため、関数内での追加操作が関数外のoriginal_dictにも反映されます。このように辞書もリスト同様、参照渡しにより関数外に影響を及ぼすことを理解しておきましょう。

6. まとめ

Pythonにおける「参照渡し」は、オブジェクトの特性に応じて動作が異なるため、コードの信頼性を向上させるためにも理解が必要です。特に、ミュータブルオブジェクトの参照渡しによる予期しない動作を避けるためには、コピーやデフォルト引数Noneを活用するのが良いでしょう。

まとめポイント

  • 値渡しと参照渡しの違い:Pythonではオブジェクトの性質に応じて、関数内での変更が元の変数に影響を与えるかが変わります。
  • イミュータブルとミュータブル:Pythonのイミュータブルオブジェクトは関数内で変更が反映されず、ミュータブルオブジェクトは参照渡しにより関数内外で共有されます。
  • 対策方法:ミュータブルなオブジェクトは予期せぬ動作を避けるために、copyモジュールのdeepcopyを使用したり、デフォルト引数をNoneに設定するなどの方法が有効です。

本記事を通じて、Pythonの参照渡しについて理解を深めていただけたかと思います。これらの知識を活用して、効率的かつバグの少ないコードを実装できるようにしましょう。