Python のデフォルト引数に datetime.now を渡してはいけない

Python

この記事は最終更新日から1年以上が経過しています。

Python の言語仕様を理解していなかったため、危険なバグを発生させた話

最初、このような関数がありました

この書き方だとテストを書くのが大変でした。
現在時刻を元にn日後、などと書けば書けなくはないのですが
例えば日や月や年をまたぐ場合など、バグが発生しそうな境界値でチェックを行う処理をテストすることは
とても複雑な条件を書いたり、datetime で返す時間を変更したりする必要がありました。

関数をこのように書き換えました

関数型プログラミングって何? という文章をちょっと前に読んでいた私は
「移せる物は引数に移せばいい」と考えました
そこで、先ほどの関数を以下のように書き換えました

これでかなりテストも書きやすくなりました。
ただ引数を増やしただけでは、元々動いていたコードが動かなくなってしまうので、
now はデフォルト値で現在時刻を取るようにしました。
これで、テスト時は now に引数を与えれば任意の現在時刻で実行でき、元のコードは何も書き換える必要がありません。

バグの発覚

その後、私はいろんなところでこの書き方をしていました
例えば、以下のような場所でも

この関数で作られたログを見ていると、おかしなことに気がつきました

なんか、時間のパターンが少ない…?
ユーザを一人退会させてから、新たなユーザを追加するまで1秒も経過してません。

時間がおかしい原因

たしか操作ID 25 から 26 の間はバグに気がついて修正し、プログラムを再起動させました。
ということは、これってプログラムを起動させた時間のまま now の値が止まっていることになります。

このように書くと、now の値は関数呼び出しの時ではなくてプログラム実行時に決定されてしまうのです。
確かに、毎回計算してたら「デフォルト引数」で設定しているのにリソースの無駄ですよね。確かに、実行時に値を固定させますよね。

ということは、最初に書いた期間チェックの関数もマズいことになります。
(再掲)

これも、購入可能かチェックするための基準時間が、プログラムを起動させた時間で固定されることになります。
つまり、いつで経っても購入終了時刻がやって来ないことになります。
…気づいて良かった。

デフォルト引数に datetime.now を渡さないように修正

これで、(あんまり綺麗ではないですが)引数になにもなければ現在時刻が与えられ、引数に現在時刻を渡すことも出来るようになりました。

おわりに

  • Python のデフォルト値はプログラム起動時に決定されます
    • これって他の言語ならばどうなるんでしょうか?
  • 今のところは↑のような書き方をしていますが、もっといい書き方をご存じの方がいれば教えて下さい

Python