LoginSignup
7
2

More than 1 year has passed since last update.

世にも奇妙なデフォルト引数の話

Posted at

この記事は?

pythonの本を読んでいたらデフォルト引数に関して気になる記載を見つけたので実際に自分でも調べてみました。
要はデフォルト引数に動的に変化するものは使うな!ってこととデフォルト引数に更新可能なものを使うな!ってことが分かります。

デフォルト引数に動的に変化するものは使うな!

デフォルト引数に動的なものを使用するとモジュールが読み込まれたタイミングでしか読まれないため都度変化させたいケースでは想定通りの動きとなりません。
実際に下記のようにコードを書き確認してみると分かります。

検証コード

time_test.py
from datetime import datetime
from time import sleep

def time_test(time_test=datetime.now()):
  print(f"time_test: {time_test}")

time_test()
sleep(1)
time_test()

time_test(datetime.now())
sleep(1)
time_test()

出力結果

time_test: 2021-10-03 17:54:41.832564
time_test: 2021-10-03 17:54:41.832564
time_test: 2021-10-03 17:54:42.845571
time_test: 2021-10-03 17:54:41.832564

分かること

モジュール読み込み時の時間がデフォルト値に設定されたら今後その設定値が変わることなく
仮に引数に値が指定された場合でも最初に設定されたデフォルト値が維持されます。

デフォルト引数に更新可能なものを使うな!

デフォルト引数に更新可能なものを使用した場合、その引数をそのまま使いまわしてしまいます。
実戦で今回検証するようなコードを書くケースは少ないかもしれませんが、安全を考えれば使用しないほうが良いかなと思いました。

検証コード

test.py
def dict_test(dict_test={'default': 1}, add_value='test'):
  print(f"dict_test: {dict_test}")
  print(f"add_value: {add_value}")
  dict_test[add_value] = add_value
  add_value = '更新したよ'

dict_test()
dict_test()
dict_test({'test3': 'test3'})
dict_test()
dict_test({'test2': 'test2'}, 'test4')
dict_test()

出力結果

dict_test: {'default': 1}
add_value: test
dict_test: {'default': 1, 'test': 'test'}
add_value: test
dict_test: {'test3': 'test3'}
add_value: test
dict_test: {'default': 1, 'test': 'test'}
add_value: test
dict_test: {'test2': 'test2'}
add_value: test4
dict_test: {'default': 1, 'test': 'test'}
add_value: test

検証から分かること

デフォルトで使った引数がそのまま使用されるので2回目の呼び出しの時は1回目の呼び出し時の'test'がキーに追加されて表示されています。
3回目の呼び出しのタイミングではデフォルト値は使用されないので引数で与えられた値が使用されてしまいます。
4回目の呼び出しではデフォルト値は2回目の呼び出しで更新されてから変わってない(1回目と同じ値なので実質1回目から変わっていない)のでそのままです。
5回目の呼び出しでデフォルト値ではない同名の辞書を更新しましたが、6回目の呼び出しでデフォルト値は2回目の呼び出しと同じでした。
また、add_valueのように更新不可能な値であれば仮に関数内で変更を加えても変わることはありませんでした。

解決策

共にデフォルト引数にはNoneを指定して関数内でNoneの場合にデフォルト値を設定するように書けば解決できます。

記載例

test.py
def time_test(time_test=None):
  if time_test is None:
    time_test = datetime.now()
  print(f"time_test: {time_test}")

def dict_test(dict_test=None, add_value='test'):
  if dict_test is None:
    dict_test = {'default': 1}
  print(f"dict_test: {dict_test}")
  print(f"add_value: {add_value}")
  dict_test[add_value] = add_value
  add_value = '更新したよ'

終わりに

今回はコメントを省略しているのですが、実際にコーディングするときはデフォルト値が何になるか?ということも補った方がよさそうですね。(読んだ本にも書いてありました…)
知っている人からしてみればそんなの当たり前だろうという内容なのかもしれないのですが、私の直感とは反するので知らないとはまってしまいそうだなと思って具体的な動きを確認して記事にしてみました。
デフォルト引数を書くときは思い出していただければと思います。

7
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2