🐡

Pythonのデフォルト引数で沼った

に公開

Python でデフォルト引数を使ったらめっちゃ沼ったので備忘録

問題のコード

datetimeモジュールを使って、10 分後を算出する関数を実装していたときのコード。
これを Flask アプリケーションの中で使おうとしていた。

from datetime import datetime, timedelta

def calculate_ten_minutes_later(current_time=datetime.now()) -> datetime:
    """現在時刻から10分後の時刻を計算する
    args:
        current_time (datetime): 現在時刻。テストコード用に引数化している
    returns:
        datetime: 現在時刻から10分後の時刻
    """
    return current_time + timedelta(minutes=10)

def main():
    ten_minutes_later = calculate_ten_minutes_later()
    log(f"10分後の時刻は: {ten_minutes_later}")

    return ten_minutes_later

Pytest でテストコードも通ったし、よし!

import pytest
from datetime import datetime, timedelta

def test_calculate_ten_minutes_later():
    fixed_time = datetime(2024, 1, 1, 12, 0, 0)
    expected_time = fixed_time + timedelta(minutes=10)
    assert calculate_ten_minutes_later(fixed_time) == expected_time

いざ Flask アプリケーションで動かしてみると…

なんか知らんけど、想定通りに動かない。
ログがずっと同じ時刻を指している。

原因の特定

原因は、デフォルト引数の評価タイミングにあった。
Python においてデフォルト引数は、関数が定義された時点で一度だけ評価される。
Flask アプリケーションが起動したときに datetime.now() が評価され、その後はずっと同じ値が使われ続けていた。

解決策&まとめ

デフォルト引数に動的な値を使う場合は、None をデフォルト値にして、関数内で評価するように変更する。

from datetime import datetime, timedelta

def calculate_ten_minutes_later(current_time=None) -> datetime:
    """現在時刻から10分後の時刻を計算する
    args:
        current_time (datetime): 現在時刻。テストコード用に引数化している
    returns:
        datetime: 現在時刻から10分後の時刻
    """
    if current_time is None:
        current_time = datetime.now()
    return current_time + timedelta(minutes=10)

ステートフルなデフォルト引数はバグの温床になるので注意!

Discussion