Python♪引数のディフォルト値が変化する場合と変化しない場合の違い

 引数のディフォルト値を設定し、関数内でその変数を変更すると、次に関数を呼び出すとき、ディフォルト値が変化する場合と変化しない場合があります。しかし、これは、理由を理解するとシンプルです。どんな場合にディフォルト値が変わってしまうのかがわかるようになります。

 なお、この記事を読む前に、以下の記事に目を通してください。この記事は様々な場面でPythonの理解の手助けをしてくれる記事です。

参考記事:「参照渡し後の要素の変更」と「参照渡し後の再定義」の違い

 今回の記事で重要なのは、以下①②です。①ディフォルト値の評価は、関数を定義した時点で一度だけ起こる。②ディフォルト値の参照は、関数の起動のたびに同じ参照先アドレスから値を呼び出すだけであり、ディフォルト値が同じ値に固定されるのではありません。

「関数の定義」と「関数の呼び出し」の違い

 まず、プログラムが実行される順番と、「関数の定義」「関数の呼び出し(コール)」のタイミングについて理解しましょう。また、「関数の定義」「関数の呼び出し(コール)」という言葉は、同じように見えてしまうかもしれませんが、そのタイミングが違いますので、その違いを理解しながら考える必要があります。

 さて、プログラムは前から順番に実行するのが原則です。コード01について、実行順序を追っていきましょう。このプログラムは出力が2ではなく、1になっているのがポイントです。「まずコードの2行目でi = 1, 関数が最初に実行される前の6行目でi = 2、だから、変数aのディフォルト値はa = 2」と考えるのは間違いです。

#コード01
i = 1
def xxx(a = i):
    print(a)

i = 2
xxx()
print('プログラムの終了')
#出力01
1

 以下、後ろが白いコードはexcel で作った力作です。矢印とか入れるのはプラグインでは難しいので頑張りました!

1.関数の前まで実行

 プログラムは、前から実行するのが原則です。プログラムのメインの部分である6行目から実行するのではありません。まずは、2行目のi = 1まで実行します。

2.「関数の定義」

 次に、関数部分の3~4行目までが実行されますが、ここで、「関数の定義」が行われます。関数xxx()がどんな関数なのかをコンピューターが理解します。しかし、「関数の定義」を行うだけなので、「関数の実行」はされません。
 なお、xxx()の括弧の中の a = iがディフォルト値の設定部分です。ディフォルト値を設定することで、関数を呼び出すときにaの値を渡さなくても値を設定することができます。
 ここで、重要なのは、ディフォルト値の評価は「関数を定義」した時点で1度だけ行われるということです。「関数の定義」を行ったタイミングではi = 1なので、aのディフォルト値は1となります。
 どうでしょうか。「関数の定義」が、どのタイミングなのかが分からなければ、頭の中がごちゃごちゃになってしまいます。

3.関数の呼び出し行の前まで実行

 関数の呼び出し行の手前6行目まで実行します。

4.関数の呼び出し(コール)

 7行目で関数が呼び出されています。「関数の呼び出し(コール)」によって、はじめて、関数が実行されます。ここでは「関数が定義」されているのではありません。「関数の定義」「関数の呼び出し(コール)」「関数の実行」を混同しないようにしましょう。

5.関数の実行

 関数の実行により、実行の行が3~4行目に移ります。aは「関数の定義」の時にa=1として評価されているので、「関数の呼び出し」の直前6行目でi = 2となっていても、a = 2にはなりません。

6.関数実行の終了

 関数の実行が終了すると、関数の呼び出し位置の直後から実行が再開されます。

 いかがですか?今回の例題のように、コードの実行の順序を意識してプログラムを読まないと、理解しにくいケースがあります。

ディフォルト値が変わる場合と変わらない場合がある

 さて、いよいよ本題です。ディフォルト値が変わってしまう場合と変わらない場合の違いを見ていきたいと思います。まずは、ディフォルト値が変わる場合です。

1.ディフォルト値が変わるケース

 コード02では、関数xxx()の変数aのディフォルト値が[3]となっています。しかし、関数内でaの要素の変更(a[0]=5)を行うことで、2回目に関数を呼び出した時にはディフォルト値が[5]になってしまいました。呼び出すたびに同じ値にしたいと思っていた人には驚きの実行結果です。

#コード02
def xxx(a = [3]):
    print(a)
    a[0] = 5

xxx()
xxx()
#出力02
[3]
[5]

 

 コード02の変数aの変化を下図にまとめました。まず、「関数の定義」のときにaのディフォルト値[3]をメモリのアドレス(i番地)に記憶します。次に「関数の呼び出し」のときに、i番地の[3]をaに読み込みます。次に関数の中で変数aの要素を変更(a[0] = 5)しています。この変更は「要素の変更」ですので、変数の参照先アドレス(i番地)は変更されません。つまり、a[0] = 5では、ディフォルト値が保管されいるi番地の値[3]が[5]に変わってしまうのです。したがって、次回、関数を呼び出すときには、変更された[5]が呼び出されます。
 ここで、重要なのは、以下①②の2点です。①ディフォルト値の評価は、関数を定義した時点で一度だけ起こる。②ディフォルト値の参照は、関数の起動のたびに同じ参照先アドレスから値を呼び出すだけであり、ディフォルト値が同じ値に固定されるのではありません。

 つまり、ディフォルト値が設定されている変数について、関数内で「要素の変更」を行うと次回関数を呼び出すときにディフォルト値が変わってしまうのです。

 ここの説明がわかりにくい人は、やはり、以下の記事を先に読みましょう。

参考記事:「参照渡し後の要素の変更」と「参照渡し後の再定義」の違い

 

2.ディフォルト値が変わらないケース

 コード03では、関数xxx()の変数aのディフォルト値は3です。しかし、コード02とは違い、関数内でaの要素の変更(a=5)としても、2回目以降のディフォルト値は3のまま変わりません。なぜ、コード02の結果と違うのでしょうか。

#コード03
def xxx(a = 3):
    print(a)
    a = 5

xxx()
xxx()
#出力03
3
3

 それは、先ほどは関数の中で「要素の変更」を行ったのに対して変数の「再定義」を行ったからです。「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。つまり、「変数の再定義」では、ディフォルト値が記憶されているi番地の値は3のまま変更されないのです。
 したがって、次に関数を呼び出すときに、ディフォルト値はi番地を参照しますので、再びa = 3がディフォルト値となります。

 つまり、ディフォルト値に変更不能体(イミュータブル)を設定した場合は、i番地の値を変更することができませんので、関数内での変更は必ず「再定義」となります。つまり、ディフォルト値が変更されることはありません。また、ディフォルト値に変更可能体(ミュータブル)を設定した場合でも、「要素の変更」(例:a[0]=5)ではなく、「再定義」(例:a = [5])を行った場合は、ディフォルト値が変更されません。

 

まとめ

 それでは、以下、「関数の定義」「関数の呼び出し」「関数の実行」の違い、ディフォルト値の設定の重要ポイント、ディフォルト値が変わる場合、変わらない場合について説明します。

1.「関数の定義」「関数の呼び出し」「関数の実行」の違い

 コンピューターが関数が記述された部分を読み込んだ時に「関数の定義」が行われます。「関数の定義」では、コンピューターが関数の内容を理解するだけで「関数の実行」は行われません。関数が実行されるのは、「関数を呼び出す」命令を実行した直後です。
 ※この記述はコードを見た方が早いかもしれません。コード01の説明を見てください。

2.ディフォルト値の設定の重要ポイント

 引数のディフォルト値を設定すると、関数を呼び出す側で設定しなくても、ディフォルト値を使って実行してくれます。もちろん、関数を呼び出す側で引数を設定すれば、ディフォルト値は無視し、設定した値で関数を実行します。

 ディフォルト値の設定で、重要なのは、以下①②の2点です。①ディフォルト値の評価は、関数を定義した時点で一度だけ行われる。②ディフォルト値の参照は、関数の起動のたびに同じ参照先アドレスから値を呼び出すだけであり、ディフォルト値が同じ値に固定されるのではありません。

3.ディフォルト値が変わる場合

(1)ディフォルト値を変更可能体(ミュータブル)とし、関数の中でその変数の「要素を変更」した場合

要素の変更の例:
 (a) a[0] = 5
 (b) a.append(5) →これが「要素の変更」になることは覚えよう。
 (c) a += [5] →これが「要素の変更」になることは覚えよう。

4.ディフォルト値が変わらない場合

(1) ディフォルト値を与えられた変数を、関数の中で「再定義」した場合。
※ディフォルト値に変更不能体を設定した場合、関数内での変更は必ず「再定義」になります。したがって、ディフォルト値が変化することはありません。

「再定義」の例:
 (a) a = 5
 (b) a = [5]
 (c) a = a + [5]
 ※左側が「変数名 =」となっているものは「再定義」です。

(2) ディフォルト値を与えられた変数を、関数の中で「再定義」した後に「要素の変更」を行た場合。
※最初に「再定義」を行った時点で、参照アドレスが別のアドレスに変わっているので、その後に「要素の変更」を行ってもディフォルト値はかわりません。

「再定義」後、「要素の変更」の例:
 (a) a = [5]の後、a[0] = 6


 いかがでしょうか。このように、「要素の変更」と「再定義」の違いを参照先アドレスが変化するかどうかに注目して理解することは重要です。引数のディフォルト値だけではありません。以下の記事も同様の考え方で考えることができます。

参考記事:
(a) 関数に引数で渡された変数の変更が、関数の外側に与える影響。
(b) 関数内からグローバル変数を変更したときに関数の外側に与える影響