Pythonにおける参照渡しと値渡しの違いについてソースコード付きで解説します。
【はじめに】参照渡しと値渡しの違い
関数を呼び出すとき、変数を引数で渡すときに「参照渡し」「値渡し」という方法があります。
それぞれの違いは以下のとおりです。
種別 | 概要 |
---|---|
値渡し(Pass by Reference) | 仮引数に指定した値をコピーして実引数に渡す |
参照渡し(Call by reference) | 仮引数に指定した値が保管されている場所の情報を実引数に渡す |
【Python】変数や関数に値を渡す場合は「参照渡し」だが注意が必要
Pythonでは、変数や関数に値を渡す場合は「参照渡し」となります。
ただし、Pythonでは「変更不可な型(Immutable)」(文字列や数値など)と「変更可能な型(Mutable)」を引数に指定した場合とで、挙動が異なるので注意が必要です。
種別 | 例 | 挙動 |
---|---|---|
変更不可な型(Immutable) | int、float、str、tuple、bytes、frozensetなど | 渡された値が変更されると元の値は変更されない(新しい領域が確保される)。 |
変更可能な型(Mutable) | list、dict、set、bytearrayなど | 渡された値が変更されると元の値も変更される(新しく領域が確保されません)。 |
変数に新たなオブジェクト自体が代入される場合、以前の参照先でなく新たなオブジェクトが作られた場所(新しく確保された領域)を参照します。つまり、オブジェクトの内容は変更できまずが、変数が保持してる参照の値は変更できません。
変更不可な型の例
サンプルコードで変更不可な型(Immutable)の例を見てみましょう。
def my_func(x): print(x, id(x)) # ② 1 140703210804896 x = x * 100 print(x, id(x)) # ③ 100 140703210808064 y = 1 print(y, id(y)) # ①1 140703210804896 my_func(y) print(y, id(y)) # ④1 140703210804896
①変数y用の領域を確保
②変数xは変数yと同じ領域を参照
③変数x用に新しい領域を確保
④変数yの値は変更されていない
Pythonのすべての値はオブジェクトです。id関数を使うことで、オブジェクトのID(識別値)を調べることができます。
変数名を引数に入れてid関数を実行すると参照しているオブジェクトのIDを表示します。異なる変数名を指定してIDが同じなら、同じオブジェクトを参照していることがわかります。
変更可な型の例
def my_func(list_x): print(list_x, id(list_x)) # ② [1, 10] 2315010976704 list_x.append(100) print(list_x, id(list_x)) # ③ [1, 10, 100] 2315010976704 list_y = [1, 10] print(list_y, id(list_y)) # ①[1, 10] 2315010976704 my_func(list_y) print(list_y, id(list_y)) # ④[1, 10, 100] 2315010976704 ← ★
①変数list_y用の領域を確保
②変数list_xはlist_yと同じ領域を参照
③変数list_xは変数list_yと同じ領域を参照 ← 変更不可な型と違うポイント!(要注意)
④変数list_yの値は変更される ← 変更不可な型と違うポイント!(要注意)
【応用】深いコピー (deep copy)
深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後に元のオブジェクト中に見つかったオブジェクトのコピーを挿入します。
Pythonではcopy.deepcopyメソッドで深いコピーを実装できます。例えば、変更を加える前のリストの状態を保存したい場合は以下のように使います。
import copy def my_func(list_x): print(list_x, id(list_x)) # ② [1, 10] 2804715309632 # オブジェクトをコピーして変更を加える list_x_copy = copy.deepcopy(list_x) list_x_copy.append(100) print(list_x, id(list_x)) # ③ [1, 10] 2804715309632 print(list_x_copy, id(list_x_copy)) # ④ [1, 10, 100] 2804715310656 return list_x_copy list_y = [1, 10] print(list_y, id(list_y)) # ① [1, 10] 2804715309632 list_y_copy = my_func(list_y) print(list_y, id(list_y)) # ⑤ [1, 10] 2804715309632 print(list_y_copy, id(list_y_copy)) # ⑥ [1, 10, 100] 2804715310656
①変数list_y用の領域を確保
②変数list_xはlist_yと同じ領域を参照
③変数list_xはlist_yと同じ領域を参照
④変数list_x_copyに新しい領域を確保
⑤変数list_xは変数list_yと同じ領域を参照
⑥変数list_y_copyは変数list_x_copyと同じ領域を参照
【注意】多重リストの初期化
例えば、2重リストlist_xを初期化して、1番目のリストlist_x[1]に1を追加したいとします。
list_x = [[0]] * 2 print(list_x, id(list_x)) # [[0], [0]] 3059227800960 list_x[1].append(1) print(list_x, id(list_x)) # [[0, 1], [0, 1]] 3059227800960 print(list_x[0], id(list_x[0])) # [0, 1] 3059226258368 print(list_x[1], id(list_x[1])) # [0, 1] 3059226258368
上記の場合、「list_x = [[0]] * 2」で参照先が同じオブジェクトを2つ生成してしまいます。
そのため、list_x[0]、list_x[1]が同じ領域を参照しています。
この場合、リスト内包表記で初期化します。
list_x = [[0] for _ in range(2)] print(list_x, id(list_x)) # [[0], [0]] 3023863098240 list_x[1].append(1) print(list_x, id(list_x)) # [[0], [0, 1]] 3023863098240 print(list_x[0], id(list_x[0])) # [0] 3023864640896 print(list_x[1], id(list_x[1])) # [0, 1] 3023864678144
【注意】関数やクラスのデフォルト引数に変更可能な型を使う場合
関数やクラスのデフォルト引数は、クラスや関数が作成されたときに一度だけ作成されます。
そのため、例えばデフォルト引数に空のリストを設定すると、以下のように関数を別々に呼び出しても、関数内の変数list_xは同じ領域を参照しているため共有化されてしまいます。
def my_func(x, list_x=[]): list_x.append(x) return list_x list_y1 = my_func(1) print(list_y1, id(list_y1)) # [1] 1695101037504 list_y2 = my_func(1) print(list_y2, id(list_y2)) # [1, 1] 1695101037504
このような現象を回避するには、デフォルト引数には、変更不可な型の値を使います。
例えば、空ならデフォルト引数にNoneを使い、Noneなら関数内で空のリストを作成します。
def my_func(x, list_x=None): if list_x==None: list_x = [] list_x.append(x) return list_x list_y1 = my_func(1) print(list_y1, id(list_y1)) # [1] 1985444654016 list_y2 = my_func(1) print(list_y2, id(list_y2)) # [1] 1985446192832
コメント