[Pythonクイズ]このコードの出力、予想できますか? PythonのちょっとしたワナPythonステップアップクイズ

普通に関数を定義しただけなのに、どうしてそうなるの? というときがありませんか? もしかしたら、このワナにハマっていませんか?

» 2025年02月25日 05時00分 公開
[かわさきしんじDeep Insider編集部]
「Pythonステップアップクイズ」のインデックス

連載目次

3つのprint関数呼び出しの出力結果はどうなる? 3つのprint関数呼び出しの出力結果はどうなる?

【問題】

 以下はリストに要素を追加して、そのリストを返送する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)

3つのprint関数呼び出しの出力結果はどうなる?

かわさき

 どうもHPかわさきです。いきなりですが、コレ、問題としてどうなんですかね。問題のコードを書きながら、「普通はこんな関数を定義しないよね」と思ったのも事実です。だって、この関数を呼ばずにtarget引数に指定するリストのappendメソッドを直接呼び出せばいいんですもん。では、なぜこんな関数を定義したのでしょうか。それがヒントです(笑)。ちょっとした頭の体操だと思って考えてみてください。

 ネコくんは「Pythonはもう分かっているニャ!」といっていますが、果たして答えが分かっているのでしょうか。


【答え】

 以下にREPL環境で問題のコードを実行した結果を示します。

3つ目のprint関数呼び出しの結果が気になる…… 3つ目のprint関数呼び出しの結果が気になる……

 1つ目の呼び出しの結果は「['a', 'b']」に、2つ目の呼び出しの結果は「['foo']」に、3つ目の呼び出しの結果は「['foo', 'bar']」になります。予想通りでしたか? 3つ目の呼び出しの結果が「[‘bar’]」になると思っていたなら、Pythonのワナにハマっていますよ!

なんでそうなるのか、分からん! なんでそうなるのか、分からん!

かわさき

 3つ目の呼び出しの結果が「['bar']」にならないのはなぜでしょう? ネコくんも困惑しているようですね。


【解説】

 さて、3つ目のprint関数呼び出しではその結果は「['foo', 'bar']」となっていました。これは、その前で行っている「append_to_target('foo')」の影響を受けていると考えられます。これはなぜなのか、以下では考えてみましょう。そのポイントとしては以下のことが挙げられます。

  • def文で関数を定義する際に、「パラメーター名=式」としてパラメーターがデフォルト引数値を持っている場合、その「式」は関数定義時に一度だけ評価され、その値がデフォルト引数値となる
  • このときに評価された式の値が、以降の関数呼び出しでは(そのパラメーターに引数が渡されない限りは)ずっと使われ続ける
  • 評価された式の値がリストのように変更可能なオブジェクトの場合、そのオブジェクトに対する変更は以降の関数呼び出しでも有効となる

 このように書くと小難しく感じてしまうので、実際にappend_to_target関数の定義を見ながら考えてみましょう。

def append_to_target(item, target=[]):
    target.append(item)
    return target

append_to_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)

2回目の呼び出し

 ここではtargetパラメーターには何も与えていません。そのため、デフォルト引数値である空のリストがtargetパラメーターの値として使われます。その結果、空のリストに要素('foo')が追加されて、それが関数の戻り値になり、print関数は「['foo']」と出力します。

 最後の呼び出しは以下でした。

result = append_to_target('bar')
print(result)

3回目の呼び出し

 もうお分かりかと思います。ここでも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入門」の「関数の引数」でも取り上げているので、興味のある方はそちらもご覧ください。


「Pythonステップアップクイズ」のインデックス

Pythonステップアップクイズ

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

Deep Insider 記事ランキング

本日月間

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。