Pythonの参照渡しの記事を書こうとしたら、早速間違い指摘されたので・・・
どうやら、Pythonでは参照渡しという言葉は厳密には正しくないようです。
「オブジェクト参照渡し (call-by-object-reference)」または「共有渡し (call-by-sharing)」が正しいようです。
Pythonは型を自動判別するので下記2つで挙動が変わる。
- ミュータブル (Mutable):
作成後に内容を変更できるオブジェクトです。代入で値が共有されます。
例:リスト (list)、辞書 (dict) - イミュータブル (Immutable):
作成後に内容を変更できないオブジェクトです。(値が共有されない)
変更するには、新しいオブジェクトを作成する必要があります。
例:数値型 (int, float)、文字列 (str)、タプル (tuple)
以下、上記の知識の元で記事を読み進めて下さい。
分かっていてもつい間違っちゃう共有した値の変更。
この辺は多くの言語でほぼ共通の挙動なだけど、特にpythonの変数やリストや辞書などの内部はすべて値(オブジェクト、データ)への参照を保持するので、代入で値を共有してしまい、変更するつもりのない値を共有して変更されてしまうバグを作ってしまう事があります。😅
というか、割とこの辺の挙動は人間の感性に合ってない記述のような気がしていて、勘で書くとこの辺の罠に引っ掛かかるので、よく考えて回避しながら書く必要があるイメージ?😗
copyは浅いコピーだから参照が共有されて元の値を変更。deepcopyは共有なしだから元の値を変更しないとか引っ掛け問題みたいなものもあるし。💢
自分への戒めのためにもその挙動を記します。
※イミュータブルオブジェクトの挙動についてもテストを追加しました。
結論としては下記のとおり。
- 共有した値の内部を編集→元の値が変わる
- 変数への再代入→元の変数・値に影響なし
- copyはこの辺の対策に微妙。元の値と完全に切り離すにはdeepcopyを使う。
- イミュータブルオブジェクトは値が共有されない
import copy
def modify_get_value(settings):
datas = settings.get("datas",{})
datas["data_a"] = 2 # 呼び出し元に影響あり(ミュータブル操作)
def modify_inner_value(settings):
settings["datas"]["data_a"] = 3 # 呼び出し元に影響あり
def overwrite_inner_dict(settings):
settings["datas"] = {"data_a": 4} # 呼び出し元に影響あり(辞書の再代入)
def overwrite_settings(settings):
settings = {"datas": {"data_a": 5}} # 呼び出し元に影響なし(settingsの再代入)
def overwrite_settings_copy1(settings):
settings_copy = settings.copy()
settings_copy["datas"]["data_a"] = 6 # 呼び出し元に影響あり(浅いコピーでは、トップレベルのオブジェクトはコピーされますが、内部のミュータブルなオブジェクト(この場合は辞書 datas)は元のオブジェクトを参照したままになります)
def overwrite_settings_copy2(settings):
settings_copy = settings.copy()
settings_copy["datas2"] = 7 # 呼び出し元に影響なし(settingsはコピーできているので、settingsの変更は元に影響しない。)
def overwrite_settings_deepcopy(settings):
settings_copy = copy.deepcopy(settings)
settings_copy["datas"]["data_a"] = 8 # 呼び出し元に影響なし(deepcopy)
def update_inner_dict(settings):
settings["datas"].update({"data_a": 9}) # 呼び出し元に影響あり(updateは中身を直接変更)
def overwrite_inner_dict_with_update(settings):
new_dict = {}
new_dict.update({"data_a": 10})
settings["datas"] = new_dict # 呼び出し元に影響あり(結局再代入)
def overwrite_value(settings):
settings = 11 # 関数内での再代入
def try_modify_tuple(data_tuple):
try:
data_tuple[0] = 100 # イミュータブルなのでエラーになる
except TypeError as e:
print(f"タプルの直接変更を試みましたがエラー: {e}")
def change_tuple_indirectly_concat(data_tuple):
try:
new_tuple = data_tuple[:1] + (100,) + data_tuple[2:]
return new_tuple
except TypeError as e:
print(f"タプルの変更(連結)でエラー: {e}")
return data_tuple
def change_tuple_indirectly_list(data_tuple):
try:
data_list = list(data_tuple)
data_list[1] = 200
new_tuple = tuple(data_list)
return new_tuple
except TypeError as e:
print(f"タプルの変更(リスト変換)でエラー: {e}")
return data_tuple
def main():
print("--- ミュータブルなオブジェクト (辞書) の場合 ---")
settings = {"datas": {"data_a": 1}}
print("初期値:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
modify_get_value(settings)
print("getした値を変更:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
modify_inner_value(settings)
print("内部の値を変更:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_inner_dict(settings)
print("内部の辞書を上書き:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_settings(settings)
print("settingsを上書き:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_settings_copy1(settings)
print("settingsの浅いコピーを変更:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_settings_copy2(settings)
print("settingsの浅いコピーに新しいキーを追加:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_settings_deepcopy(settings)
print("settingsのdeepcopyを変更:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
update_inner_dict(settings)
print("updateで中身変更:", settings)
settings = {"datas": {"data_a": 1}} # settingsをリセット
overwrite_inner_dict_with_update(settings)
print("updateで作った辞書で上書き:", settings)
print("\n--- イミュータブルなオブジェクト (数値) の場合 ---")
num = 1
print("初期値:", num)
overwrite_value(num)
print("数値を上書きしたつもり:", num)
print("\n--- イミュータブルなオブジェクト (文字列) の場合 ---")
text = "ABC"
print("初期値:", text)
overwrite_value(text) # 関数内では再代入されるが、元の変数は変わらない
print("文字列を上書きしたつもり:", text)
print("\n--- イミュータブルなオブジェクト (タプル) の場合 ---")
data = (1, 2, 3)
print("初期値:", data)
try_modify_tuple(data)
result = change_tuple_indirectly_concat(data)
print("連結後のタプル:", result)
print("連結後のタプル(引数への変更なし):", data)
result = change_tuple_indirectly_list(data)
print("リスト変換後のタプル:", result)
print("リスト変換後のタプル(引数への変更なし):", data)
if __name__ == "__main__":
main()
結果は下記のとおりです。
--- ミュータブルなオブジェクト (辞書) の場合 ---
初期値: {'datas': {'data_a': 1}}
getした値を変更: {'datas': {'data_a': 2}}
内部の値を変更: {'datas': {'data_a': 3}}
内部の辞書を上書き: {'datas': {'data_a': 4}}
settingsを上書き: {'datas': {'data_a': 1}}
settingsの浅いコピーを変更: {'datas': {'data_a': 6}}
settingsの浅いコピーに新しいキーを追加: {'datas': {'data_a': 1}}
settingsのdeepcopyを変更: {'datas': {'data_a': 1}}
updateで中身変更: {'datas': {'data_a': 9}}
updateで作った辞書で上書き: {'datas': {'data_a': 10}}
--- イミュータブルなオブジェクト (数値) の場合 ---
初期値: 1
数値を上書きしたつもり: 1
--- イミュータブルなオブジェクト (文字列) の場合 ---
初期値: ABC
文字列を上書きしたつもり: ABC
--- イミュータブルなオブジェクト (タプル) の場合 ---
初期値: (1, 2, 3)
タプルの直接変更を試みましたがエラー: 'tuple' object does not support item assignment
連結後のタプル: (1, 100, 3)
連結後のタプル(引数への変更なし): (1, 2, 3)
リスト変換後のタプル: (1, 200, 3)
リスト変換後のタプル(引数への変更なし): (1, 2, 3)
copy,deepcopyの挙動についてはコメント欄にも有用な情報を書き込んで頂けたのでご確認願います。