【Python】参照渡し・値渡しのような動作と注意点

Pythonにおける参照渡し・値渡しのような動作と注意について解説します。

1. 参照渡し (Pass by Reference)のような動作

Pythonでは、関数に引数を渡すと、その引数のオブジェクトの参照(アドレス)が渡されます。
つまり、関数内で引数の値を変更すると、元の変数にも影響を与えます。

Pythonでは、関数へ渡す引数がミュータブル(変更可)なオブジェクト(リスト、辞書、セットなど)である場合、「参照渡しのような動作」が見られます。

def modify_list(lst):
    lst.append(4)
    print("関数内:", lst) # 「関数内:[1, 2, 3, 4]」と表示される

my_list = [1, 2, 3]
modify_list(my_list)
print("関数外:", my_list) # 「関数外:[1, 2, 3, 4]」と表示される

上記の例では、関数内でリストlstを変更すると、元のリストmy_listにもその変更が反映されます。

注意

通常の参照渡しとは、変数そのものを関数に渡すことを指します。
そのとき、関数内で引数の値を変更すると、元の変数にも影響を与えます。

Pythonでは「変数のオブジェクトの参照」を渡します。
つまり、渡した先の関数内で使用される変数は、元のオブジェクトを指しています。

そして、そのオブジェクトがミュータブル(変更可)な場合、関数内での操作が元のオブジェクトにも影響を及ぼし、参照渡しのような動作をします。
そのため、Pythonにおける「参照渡しのような動作」は、厳密には「通常の参照渡し」と異なります。

2. 値渡し(Pass-by-Value)のような動作

Pythonでは、関数へ渡す引数がイミュータブルなオブジェクト(数値、文字列、タプルなど)の場合、値渡しのような動作が見られます。

def modify_value(x):
    x = 10
    print("関数内:", x) # 「関数外:10」と表示される

a = 5
modify_value(a)
print("関数外:", a) # 「関数外:5」と表示される

上記の例では、関数内でxを変更しても、元の変数aには影響がありません。

注意点

通常の値渡しとは、関数に引数を渡す際に、実際の値のコピーを渡すことを指します。
そのとき、関数内で引数を変更しても、元の値には影響を与えません。

Pythonでは「変数のオブジェクトの参照」を渡します。
つまり、渡した先の関数内で使用される変数は、元のオブジェクトを指しています。

そして、そのイミュータブル(変更不可)なオブジェクトである場合、関数内でオブジェクト自体が新しく作り直され、関数内での操作は元のオブジェクトに影響を与えないため、値渡しのような動作をします。

ちなみに、Python公式ドキュメントでは、「オブジェクトの参照を値渡し(オブジェクトの参照渡し)」であると記述されています。

Python公式ドキュメント「4. その他の制御フローツール」では以下のように記述されています。

関数を呼び出す際の実際の引数 (実引数) は、関数が呼び出されるときに関数のローカルなシンボルテーブル内に取り込まれます。そうすることで、実引数は 値渡し (call by value) で関数に渡されることになります (ここでの 値 (value) とは常にオブジェクトへの 参照(reference) をいい、オブジェクトの値そのものではありません) [1]。ある関数がほかの関数を呼び出すときや、自身を再帰的に呼び出すときには、新たな呼び出しのためにローカルなシンボルテーブルが新たに作成されます。

(略)

脚注
[1]
実のところ、オブジェクトへの参照渡し (call by object reference) という言ったほうがより正確です。というのは、変更可能なオブジェクトが渡されると、呼び出された側の関数がオブジェクトに行った変更 (例えばリストに挿入された要素) はすべて、関数の呼び出し側にも反映されるからです。

3. デフォルト引数の罠

デフォルト引数としてミュータブルなオブジェクトを使用すると、予期しない動作が発生することがあります。
デフォルト引数は関数が定義された時点で一度だけ評価されるため、複数回の関数呼び出しで同じオブジェクトが共有されます。

def add_to_list(value, lst=[]):
    lst.append(value)
    return lst

print(add_to_list(1))  # [1]
print(add_to_list(2))  # [1, 2]

この場合、lstは関数呼び出し間で共有されてしまいます。
これを避けるには、デフォルト引数としてNoneを使用し、関数内で新しいリストを作成します。

def add_to_list(value, lst=None):
    if lst is None:
        lst = []
    lst.append(value)
    return lst

print(add_to_list(1))  # [1]
print(add_to_list(2))  # [2]

4. シャローコピーとディープコピー

オブジェクトのコピーを作成する際、シャローコピー(浅いコピー)とディープコピー(深いコピー)があります。
シャローコピーはオブジェクトの参照をコピーするだけで、ディープコピーはオブジェクト自体を再帰的にコピーします。

import copy

original_list = [1, [2, 3]]
shallow_copy = copy.copy(original_list)
deep_copy = copy.deepcopy(original_list)

original_list[1][0] = 99
print(shallow_copy)  # [1, [99, 3]]
print(deep_copy)     # [1, [2, 3]]

関連ページ

【Python超入門】使い方とサンプル集
Pythonの使い方について、基礎文法から応用例まで入門者向けに解説します。

コメント

  1. juner より:

    Pythonには参照渡しは無いのではないでしょうか?

    公式ドキュメントにも無いと明言されています。

    ここで言っている参照渡しはミュータブルなインスタンスを渡しているだけでは……(共有渡しともいわれるものでは)

  2. juner より:

    > 渡された値が変更されると元の値は変更されない(新しい領域が確保される)。

    とありますが、変更手段が用意されていないならそもそも変更できないのではないでしょうか……?