ちゃお・・・†
最近Pythonのデフォルト引数値による意図せぬ挙動に悩まされたので、同じ過ちを繰り返さぬようここで情報を共用したいと思います。こちらの環境はPython 3.7.7と3.8.3です。
デフォルト引数値とは
下記コードのうち dt=datetime.now()
を指します。
from datetime import datetime
def show_second(dt=datetime.now()):
print(dt.second)
Pythonのデフォルト引数値の罠
先ほどのコードを、 show_second
関数を一度呼び出し、三秒後にまた show_second
関数を呼び出すようにいじってみました。
import time
from datetime import datetime
def show_second(dt=datetime.now()):
print(dt.second)
show_second() #=> 23
time.sleep(3)
show_second() #=> 23
すると、なんということでしょう!三秒間sleepしたにも関わらず二度目の show_second
関数呼び出し時にプリントされる値が三秒前と同じでした…!?時が止まった?ザ・ワールド???新手のスタンド使いか???
Pythonのデフォルト引数値の挙動
さて、これはどういうことかとPythonのドキュメントを読んでみたところ、下記の記述がありました。
デフォルト引数値は関数定義が実行されるときに左から右へ評価されます。 これは、デフォルト引数の式は関数が定義されるときにただ一度だけ評価され、同じ "計算済みの" 値が呼び出しのたびに使用されることを意味します。
from: https://docs.python.org/ja/3/reference/compound_stmts.html#function-definitions
つまり、関数定義時にデフォルト引数値は一度評価され、その結果がメモリーに保存されて、以降はその関数を何回呼び出してもデフォルト引数値を利用する場合は関数定義時の評価結果を使用するという仕組みになっているようです。
なので、datetime.now()
のようなcallする度に違う結果を出すもの and/or リアルタイム性が求められるものをデフォルト引数値として用いるのは危険です⚠️
同様にデフォルト引数値にリストや辞書を指定したときも気をつける必要があるようです。(Pythonの関数でのデフォルト引数の使い方と注意点 | note.nkmk.me)
Pythonのデフォルト引数値の対策
それではどうしたらいいかというと、デフォルト引数値に None
を設定して、None
であれば本来デフォルト引数値として設定したかった値を代入するという風にするのが良いそうです。
以下example codeです。
import time
from datetime import datetime
def show_second(dt=None):
if dt is None:
dt = datetime.now()
print(dt.second)
show_second() #=> 23
time.sleep(3)
show_second() #=> 26
そんなわけでPythonのデフォルト引数値に気をつけようというお話でした。