😵

Pythonのデフォルト引数の挙動

2021/07/04に公開
  • 知っている人は知っている、Pythonのデフォルト引数の挙動
  • 他言語のノリで使うと地味にハマる
  • Python歴3ヶ月で見事に引っかかった
  • 「Python デフォルト引数」でググると、注意・罠・ハマるとか色々出てくる

検証

下記は、sample関数実行 → 3秒スリープ → sample関数実行を行うだけのコードです。
sample関数のデフォルト引数に datetime.now() が設定されています。

sample.py
from datetime import datetime
from time import sleep

def sample(message, now=datetime.now()):
    print('{}: {}'.format(message, now.strftime('%Y-%m-%d %H:%M:%S')));

print('START: {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
sample(message='1回目');
sleep(3);
sample(message='2回目');
print('END  : {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))

実行

$ python sample.py
START: 2018-09-16 21:34:00
1回目: 2018-09-16 21:34:00
2回目: 2018-09-16 21:34:00
END  : 2018-09-16 21:34:03

実行してみると、何故か3秒スリープしているにも関わらず、1回目と2回目の now が同じです。
now なのに now じゃない。。。
なんでや。。。

公式リファレンス

Python のリファレンスに記載がありました。

https://docs.python.org/ja/3/reference/compound_stmts.html#function-definitions

デフォルト引数値は関数定義が実行されるときに左から右へ評価されます。
これは、デフォルト引数の式は関数が定義されるときにただ一度だけ評価され、
同じ "計算済みの" 値が呼び出しのたびに使用されることを意味します。

一度、評価された内容が次回以降も参照される為、1回目と2回目の now が同じだったようです。
デフォルト引数で datetime.now() をしないようにすれば良いので、
関数内で datetime.now() を実行するように変更すれば、とりあえず解決します。
これ以外にも良い方法があるかもしれませんが。

sample.py
def sample(message, now=None):
    now = datetime.now() if now is None else now
    print('{}: {}'.format(message, now.strftime('%Y-%m-%d %H:%M:%S')));

余談

Nginx + uWSGI + Django で作ってたんですが、
この事象が発生した当初、uWSGI + Django のどっかが原因だろうと疑ってました。
結局見当違いだったので、申し訳なさが半端ないです。

Discussion