普通に関数を定義しただけなのに、どうしてそうなるの? というときがありませんか? もしかしたら、このワナにハマっていませんか?
以下はリストに要素を追加して、そのリストを返送するappend_to_target関数の定義とそれを呼び出すコードである。3つのprint関数呼び出しはappend_to_target関数の戻り値を出力するが、これらはどのような値になるだろうか。
def append_to_target(item, target=[]):
target.append(item)
return target
t = ['a']
result = append_to_target('b', t)
print(result)
result = append_to_target('foo')
print(result)
result = append_to_target('bar')
print(result)
どうもHPかわさきです。いきなりですが、コレ、問題としてどうなんですかね。問題のコードを書きながら、「普通はこんな関数を定義しないよね」と思ったのも事実です。だって、この関数を呼ばずにtarget引数に指定するリストのappendメソッドを直接呼び出せばいいんですもん。では、なぜこんな関数を定義したのでしょうか。それがヒントです(笑)。ちょっとした頭の体操だと思って考えてみてください。
ネコくんは「Pythonはもう分かっているニャ!」といっていますが、果たして答えが分かっているのでしょうか。
以下にREPL環境で問題のコードを実行した結果を示します。
1つ目の呼び出しの結果は「['a', 'b']」に、2つ目の呼び出しの結果は「['foo']」に、3つ目の呼び出しの結果は「['foo', 'bar']」になります。予想通りでしたか? 3つ目の呼び出しの結果が「[‘bar’]」になると思っていたなら、Pythonのワナにハマっていますよ!
3つ目の呼び出しの結果が「['bar']」にならないのはなぜでしょう? ネコくんも困惑しているようですね。
さて、3つ目のprint関数呼び出しではその結果は「['foo', 'bar']」となっていました。これは、その前で行っている「append_to_target('foo')」の影響を受けていると考えられます。これはなぜなのか、以下では考えてみましょう。そのポイントとしては以下のことが挙げられます。
このように書くと小難しく感じてしまうので、実際にappend_to_target関数の定義を見ながら考えてみましょう。
def append_to_target(item, target=[]):
target.append(item)
return target
このコードでは「target=[]」として、targetパラメーターのデフォルト引数値を空のリスト([])としています。上で挙げたポイントによれば、この式「[]」は関数定義時にだけ評価されます。つまり、関数を定義する時点で、空のリストが初期化されて、その後で関数が呼び出される際には常にそれが使い回されるということです。
そして、この関数の最初の呼び出しは次のようなものでした。
t = ['a']
result = append_to_target('b', t)
print(result)
ここではリストtを定義して、それをtargetパラメーターに渡しています。そのため、関数定義時に作られた空のリストは使われません。リストtに要素が追加され、それが戻り値として返送されます。そのため、print関数では「['a', 'b']」が出力されます。
次の呼び出しは以下です。
result = append_to_target('foo')
print(result)
ここではtargetパラメーターには何も与えていません。そのため、デフォルト引数値である空のリストがtargetパラメーターの値として使われます。その結果、空のリストに要素('foo')が追加されて、それが関数の戻り値になり、print関数は「['foo']」と出力します。
最後の呼び出しは以下でした。
result = append_to_target('bar')
print(result)
もうお分かりかと思います。ここでもtargetパラメーターには何も与えていません。そのため、先ほど要素('foo')を追加したリストがtargetパラメーターの値となります。そして、これに今度は要素として'bar'が追加され、それが戻り値になります。その結果、print関数は「['foo', 'bar']」を出力したのです。
このようにデフォルト引数値に変更可能なオブジェクトを記述すると、それが想定外の事態を引き起こすことがあります。では、こうした事態を引き起こさないようにするにはどうしたらよいかというと、Pythonのドキュメントではデフォルト引数値にはNoneを指定することが推奨されています。そして、対応するパラメーターに何も渡されなかったときには、そのパラメーターに関数内で変更可能なオブジェクトを割り当てます。
上のコードを修正するのであれば次のようになるでしょう。
def append_to_target(item, target=None):
if target is None:
target = []
target.append(item)
return target
このようにすることで、以前の関数呼び出しの影響を受けないようになるはずです。実際に修正後のコードで同じことをしてみた結果を以下に示します。
ここを理解すると、ネコくんも「Python、ちょっと分かる」と言い出すかもしれません(でも、先はまだまだ長いんですよねぇ)。
このように「これやっちゃダメよ」というコードの書き方を「アンチパターン」などと呼ぶことがあります。プログラミング言語ごとにアンチパターンはいろいろとあるので、これからも問題として取り上げていく予定です。
なお、デフォルト引数値については「Python入門」の「関数の引数」でも取り上げているので、興味のある方はそちらもご覧ください。
初心者向け、データ分析・AI・機械学習・Pythonの勉強方法 @ITのDeep Insiderで学ぼう
Copyright© Digital Advantage Corp. All Rights Reserved.
Deep Insider 記事ランキング