Pythonのdatetime.timedeltaで秒を扱う際に注意すべきたったひとつのこと

Pythonのdatetime.timedeltaで秒を取得する時のために覚えておいてください。
2022.03.17

こんにちは。サービスグループの武田です。

みなさんプログラムで日時の扱いは得意でしょうか?基本的には言語の標準ライブラリを使うことが多いのではないでしょうか。

今回はPythonのお話です。Pythonは標準でdatetimeモジュールが提供され、日付や時間、タイムゾーンなどを扱えます。その中にtimedeltaオブジェクトというものが提供されているのですが、これは 時刻間の差 を表すという、他の言語ではあまり見ないユニークな機能を提供します。

使い方の例を次に示します。+演算子などでdatetimeオブジェクトと演算でき、直感的に日時の操作が可能です。

from datetime import datetime, timedelta

d = datetime.fromisoformat("2004-07-07T00:00:00")

d #=> datetime.datetime(2004, 7, 7, 0, 0)
d + timedelta(seconds=30) #=> datetime.datetime(2004, 7, 7, 0, 0, 30)
d + timedelta(hours=8) #=> datetime.datetime(2004, 7, 7, 8, 0)

さてこのtimedeltaオブジェクトですが、secondsという読み出し用の属性を持っています。名前から分かるとおり、表している時刻間の差を秒で取得できます。

timedelta(seconds=30).seconds #=> 30
timedelta(minutes=5).seconds #=> 300
timedelta(hours=8).seconds #=> 28800

(timedelta(hours=8) * 4).seconds #=? ???

なるほど、便利ですね。最後の結果は伏せたのですが、結果を予想できるでしょうか?

8時間は28800秒ですね。その前の結果からわかります。それを4倍しているのだから 115200だ! と考えたあなた!間違いです!

結果は 28800 になります。

はて、どういうことなのか。

詳細はドキュメントを読んでもらうとして重要な点だけ抜粋します。

days, seconds, microseconds だけが内部的に保持されます。

さらに、値が一意に表されるように days, seconds, microseconds が以下のように正規化されます
0 <= microseconds < 1000000
0 <= seconds < 3600*24 (一日中の秒数)
-999999999 <= days <= 999999999

datetime --- 基本的な日付型および時間型 — Python 3.10.0b2 ドキュメント

そうなんです。24時間までは秒で保持されるのですが、超えると内部的には秒ではなく日として保存されます。さきほどのseconds属性はあくまで保持している秒属性を返すだけなんですね。事実(timedelta(hours=8) * 4).daysの結果は1を返します。

そんなわけでこのエントリの言いたい たったひとつのこと とは、timedeltaが保持している時間差を正しく取得したいならtotal_seconds()を使ってください!ということです。

(timedelta(hours=8) * 4).total_seconds() #=> 115200.0

ただしこのメソッドはmicrosecondsも含むためfloat型を返します。秒だけでいいのであればint()などで小数部分は切り捨てましょう。

まとめ

24時間を超えない限りはsecondsで秒を取得していても問題ありません。しかし24時間を超えた値を扱った時にバグとして表面化します。仕様として超える可能性があるのであればtotal_seconds()を使ってバグを作り込まないように気をつけてください。