【Python】関数の引数にデフォルト値を設定した時の考察
# はじめに
Python入門者の後輩にグローバル変数とローカル変数について教える参考資料として、「Pythonの変数スコープの話」という記事を使わせて頂いたのですが、その中に「クラスで定義する変数」という項目があり、その中で紹介してあるコードについて検証したので備忘録として書いています。
# 問題のコード
例えば、こんなクラス↓があったとします。
このクラスを使って次のようなコードを作ったとします。
このコードを実行すると次のようになります。
$ python 20180228_sample2.py ------(1)------ [t1]: ['test'] [t2]: None ------(2)------ [t1]: ['test', 'test'] [t2]: ['test', 'test']
これを見てわかる通り、(1)は期待通りなのですが(2)は期待通りの結果ではありません。
# 実験
この挙動について調べるために、id関数を使用してメモリアドレスを調べて見ました。 そのためのコードが次の通りです。
このコードを実行すると次のようになります。
$ python 20180228_sample3.py None :0x01022877A8 t1 :0x0102B0F780 t2 :0x0102B0F7B8 t1.set() val :0x0102B0DC08 common :0x01022877A8 # Noneのアドレス先 val :0x0102B0DC08 common :0x0102B0DC08 ------(1)------ [t1]: ['test'] [t2]: None t2.set() val :0x0102B0DC08 common :0x01022877A8 # Noneのアドレス先 val :0x0102B0DC08 common :0x0102B0DC08 ------(2)------ [t1]: ['test', 'test'] [t2]: ['test', 'test']
これからt1とt2は違うインスタンスが生成されているのですが、 Hoge.set関数の引数で渡しているval変数は同じアドレスであることが分かります。
# ドキュメントに振り返ろう
まぁよく言うことなんですが、「まずは公式ドキュメントに振り返ろう」。 ということで、公式ドキュメントでデフォルトの引数値について調べるとちゃんと次のような一文があります。
[原文]
Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. 『4. More Control Flow Tools — Python 3.6.4 documentation』より抜粋
[日本語翻訳版]
重要な警告: デフォルト値は 1 度だけしか評価されません。デフォルト値がリストや辞書のような変更可能なオブジェクトの時にはその影響がでます。『4. その他の制御フローツール — Python 3.6.4 ドキュメント』より抜粋
ということで、ちゃんと載っています。
# もう一歩検証
まぁ、これまでからデフォルト値は1度だけしか評価されないことが分かりました。 では、変数valをdelしたり、clear関数を呼んだり、[]で初期化したり、self.commonにvalをコピーしたらどうなるのか。 それを試してました。
その結果がこちら
$ python 20180228_sample4.py ====<<< Hoge1 >>>==== None :0x010FF9D7A8 t1 :0x0110841400 t2 :0x0110841470 t1.set() val :0x0110823C08 common :0x010FF9D7A8 val :0x0110823C08 common :0x0110823C08 common :0x0110823C08 ------(1)------ [t1]: ['test'] [t2]: None t2.set() val :0x0110823C08 common :0x010FF9D7A8 val :0x0110823C08 common :0x0110823C08 common :0x0110823C08 ------(2)------ [t1]: ['test', 'test'] [t2]: ['test', 'test'] ====<<< Hoge2 >>>==== None :0x010FF9D7A8 t1 :0x0110841470 t2 :0x01108414E0 t1.set() val :0x011086EA48 common :0x010FF9D7A8 val :0x011086EA48 common :0x011086EA48 val :0x011086EA48 common :0x011086EA48 ------(1)------ [t1]: [] [t2]: None t2.set() val :0x011086EA48 common :0x010FF9D7A8 val :0x011086EA48 common :0x011086EA48 val :0x011086EA48 common :0x011086EA48 ------(2)------ [t1]: [] [t2]: [] ====<<< Hoge3 >>>==== None :0x010FF9D7A8 t1 :0x01108414E0 t2 :0x0110841438 t1.set() val :0x011086EA08 common :0x010FF9D7A8 val :0x011086EA08 common :0x011086EA08 val :0x011086E948 common :0x011086EA08 ------(1)------ [t1]: ['test'] [t2]: None t2.set() val :0x011086EA08 common :0x010FF9D7A8 val :0x011086EA08 common :0x011086EA08 val :0x011086E948 common :0x011086EA08 ------(2)------ [t1]: ['test', 'test'] [t2]: ['test', 'test'] ====<<< Hoge4 >>>==== None :0x010FF9D7A8 t1 :0x0110841438 t2 :0x0110841470 t1.set() val :0x011086E9C8 common :0x010FF9D7A8 val :0x011086E9C8 common :0x011086E948 ------(1)------ [t1]: ['test'] [t2]: None t2.set() val :0x011086E9C8 common :0x010FF9D7A8 val :0x011086E9C8 common :0x0110823F88 ------(2)------ [t1]: ['test'] [t2]: ['test', 'test'] ====<<< Hoge5 >>>==== None :0x010FF9D7A8 t1 :0x0110841470 t2 :0x01108414E0 t1.set() val :0x011086E988 common :0x010FF9D7A8 val :0x011086E988 common :0x0110823F88 val :0x011086E948 common :0x0110823F88 ------(1)------ [t1]: ['test'] [t2]: None t2.set() val :0x011086E988 common :0x010FF9D7A8 val :0x011086E988 common :0x011086E948 val :0x011086EAC8 common :0x011086E948 ------(2)------ [t1]: ['test'] [t2]: ['test', 'test']
ということで、思い通りの結果になったのはvalをself.commonにコピーした時でした。
# 結論
まぁ、デフォルト値を使うならいきなりaddpend関数で要素を足すというのが、まずどうなんだろう...って感じがしますので、 まずはあのようなコードは書かないようにするのが大切かもしれないですね。