【Pythonの落とし穴】参照渡しと対処法
Pythonでは、関数に引数を渡す場合デフォルトで参照渡しという方法を採用しています。
これは渡した変数の値ではなく、アドレスを渡すことに対応しています。この点を理解していないと、予期せぬ変数の更新などに繋がる可能性があり、プログラムのアウトプットが思わぬ結果となってしまいます。
ここではこのような問題を再現し、copyライブラリを使った対処法を紹介します。
イミュータブルなデータを渡す場合
Pythonにおけるデータは大きく分けてイミュータブル(=変更不可)なものとミュータブル(=変更可)なものに分類されます。ここではイミュータブルなデータの代表としてint型を渡す場合について考えます。
サンプルとして引数に100を足して返す関数を用意します。
def add100int(a):
b = a
b += 100
return b
5を格納したint型変数xを、サンプル関数に渡してみます。
x = 5
y = add100int(x)
print(f'X = {x} and Y = {y}')
# x = 5 and y = 105
関数に渡した変数xはイミュータブルな為、関数外では元の値5を保持したままとなります。
ミュータブルなデータを渡す場合
次にミュータブルなデータの代表としてlist型を渡す場合について考えます。
今回のサンプル関数は、引数aをlist型と想定しています。
関数内でlist型の第一要素に100を足して返します。また、引数aを一度異なるデータbに格納しています。
def add100list(a):
b = a
b[0] += 100
return b
例として、5を第一要素に格納したlist型変数xを、サンプル関数に渡してみます。
x = [5]
y = add100list(x)
print(f'X = {x} and Y = {y}')
# x = [105] and y = [105]
関数内でxをbに代入して100を足したはずが、関数外の変数xにも100が足されています。これがPythonにおけるミュータブルなデータを関数に渡した際の、デフォルトの挙動となります。この点を押さえておかなければ、xは[5]のままであると勘違いしたままプログラムを書いてしまいかねません。
またこの場合のプログラムは特にエラーなどが出ないので、プログラムが大きければ大きいほど問題を見つけるのが非常に困難となります。
ミュータブルなデータを渡す場合: copyライブラリを使った対処法
参照渡しに伴うこの問題は、pythonのcopyライブラリを使うことで解決できます。データを他のデータに格納する際に、変数のアドレスを渡す代わりに他のメモリを用意し値をコピーすることで、予期せぬ上書きを避けることが出来るのです。
以下のサンプルコードを見てください。今回は引数aをbに代入する際に、copyライブラリを使っています。
import copy
def add100list_copy(a):
b = copy.copy(a)
b[0] += 100
return b
list型変数を、サンプルのadd100list_copy()関数に渡してみます。
x = [5]
y = add100list_copy(x)
print(f'X = {x} and Y = {y}')
# x = [5] and y = [105]
ミュータブルなlist型変数xを渡しても、copyライブラリを使ったaのbへの代入によって、xが関数外で上書きされていないことが分かります。
ミュータブルなデータを再帰的に格納したデータを渡す場合
ここまで、参照渡しに伴うミュータブルな変数の予期せぬ上書きを、copy.copy()で防ぐ方法を紹介しました。
しかし、この関数で行われるコピーは「浅いコピー」と呼ばれ、コピーしたデータのより深い階層では、2変数間でアドレスを共有したままとなります。例として、list型を要素にもつlist型変数を渡す場合を考えましょう。
サンプルコードを用意しました。引数aは、list型を要素に持つlist型変数と想定しています。サンプル関数は、要素[0][0]に100を足して返します。
前回同様、引数aをcopy.copy()でコピーしたものを、bに代入してみましょう。
def add100listlist_copy(a):
b = copy.copy(a)
b[0][0] += 100
return b
list型を要素に持つlist型変数を、サンプルのadd100listlist_copy()に渡してみましょう。
x = [[5]]
y = add100listlist_copy(x)
print(f'X = {x} and Y = {y}')
# x = [[105]] and y = [[105]]
今回はcopy.copy()を使ったにも関わらず、関数外にある変数xが5から105に更新されています。これは前述の通り、copy.copy()が「浅いコピー」であり、xの第一要素に格納されたリスト[5]のアドレスは依然として関数内のaとbで共有されていることに由来します。
ミュータブルなデータを再帰的に格納したデータを渡す場合: 「深いコピー」を使った対処法
前述の問題は、copyライブラリの「深いコピー」copy.deepcopy()関数を使用することで解決できます。
aをbに代入する際に「深いコピー」を行うcopy.deepcopy()を用いたサンプル関数を用意しました。「深いコピー」によって、変数内の構造について再起的に独立したアドレスを生成し、値をそれぞれコピーすることが出来ます。
def add100listlist_deepcopy(a):
b = copy.deepcopy(a)
b[0][0] += 100
return b
list型を要素に持つlist型変数を渡してみます。
x = [[5]]
y = add100listlist_deepcopy(x)
print(f'x = {x} and y = {y}')
# x = [[5]] and y = [[105]]
copy.deepcopy()を使って「深いコピー」を行うことで、関数外の変数xの変更を防ぐことが出来ました。
Startlabのプログラミング入門コースは、Python未経験者・初学者にもわかりやすいカリキュラムと続けやすいサポート体制が魅力。自分の学習目的に合っているか、どういった知識を身に付けることができるのかなど、無料カウンセリングでぜひご相談ください!カウンセリングは毎日実施中、お待ちしております!