デフォルト引数に関数を使う場合の注意点
Pythonの関数定義に欠かせないデフォルト引数ですが、とある罠にひっかかったので備忘録として残しておきます。
まずは、以下のようなコードを用意します
# coding: utf-8 import datetime import time def arg_test(message: str, now=datetime.datetime.now()): print(message) print(now) if __name__ == "__main__": arg_test('デフォルト引数利用1') arg_test('デフォルト引数非利用1', datetime.datetime.now()) time.sleep(1.5) arg_test('デフォルト引数利用2') arg_test('デフォルト引数非利用2', datetime.datetime.now())
datetime.datetime.now()
は、現在時を取得するコード、 time.sleep(1.5)
は1.5秒待機する簡単なコードです。
- デフォルト引数を利用、非利用で関数をそれぞれ1回実行。非利用時は、引数で現在時を入力。
- 1.5秒待機
- 1と同じことをする。
ここまでくればなんとなくわかるでしょう。
このデフォルト引数の関数が評価されるタイミングを僕は見誤っていたという話になります。
ちなみに、実行結果は以下のようになります。(実行時は 2021-12-20 14:38:16.655634
)
デフォルト引数利用1 2021-12-14 20:38:16.655634 デフォルト引数非利用1 2021-12-14 20:38:16.655696 待機開始 待機終了 デフォルト引数利用2 2021-12-14 20:38:16.655634 デフォルト引数非利用2 2021-12-14 20:38:18.156938
デフォルト引数を利用した方は、スリープしても表示される現在時が一切変わりません。
つまり、デフォルト引数に関数を渡した場合は、普通に引数に関数を渡した場合と関数の評価タイミングが異なるということになります。
Pythonの公式には以下のように記載されています。
デフォルト引数値は関数定義が実行されるときに左から右へ評価されます。
とあります。つまり、関数を定義する際に引数の関数は実行され、 その時の値がひたすら使い回されるという形になります。
まだ単体のすぐ終わるスクリプトならいいですが、webフレームワークを利用してるときなどは、サーバ開始時の評価結果がひたすら入るので注意したほうがいいですね。
また、インスタンスなどを作成する際も以下のような事故は起こりそうで個人的には怖いなと思いました。
# coding: utf-8 from dataclasses import dataclass outer_target = 4 @dataclass class ArgTestClass(): target_arg: int = 3 def update_arg(self, num: int): self.target_arg = num def print_arg(self, target=target_arg): print(target) def print_outer_target(self, target=outer_target): print(target) if __name__ == "__main__": ins: ArgTestClass = ArgTestClass() print('更新前') ins.print_arg() ins.print_outer_target() ins.update_arg(5) outer_target = 6 print('更新後') ins.print_arg() ins.print_outer_target()
実行結果
更新前 3 4 更新後 3 4
こちらも定義後に値を更新してるけど更新されないといった形ですね。インスタンス変数をデフォルト値にいれるのはあんまりないかもしれませんが、外部のスコープの方はなんとなく使ってたらやらかしそうな感じがします。
僕は、デフォルト値にはできるだけ静的な値を使うを心がけて生きていこうと思います。