本記事のポイント
Datetime型にはタイムゾーン情報を付けましょう。とりあえず以下のような関数を使うのが良いかと思います。
from datetime import datetime
from zoneinfo import ZoneInfo
def get_utc_now():
return datetime.now(ZoneInfo("UTC"))
def get_jst_now():
return datetime.now(ZoneInfo("Asia/Tokyo"))
def parse_str_as_jst(dt_str, dt_format, dt_timezone="Asia/Tokyo"):
return datetime.strptime(dt_str, dt_format).astimezone(ZoneInfo(dt_timezone))
はじめに
アプリケーションをサーバーで動かすとなれば、タイムゾーンの管理が問題となります。仮に日本国内でサービス提供するとしても、サーバー時刻がUTCであれば考慮が必要になります。そんな日時オブジェクトの扱い方・注意点についてまとめました。
Pythonの標準ライブラリであるdatetimeを中心にまとめていきます。また、タイムゾーン情報を扱うライブラリにはzoneinfoやpytzが存在します。zoneinfoが標準ライブラリであるため、こちらを利用します。(Python3.9で追加された)
Native / Aware datetimeの違い
日時オブジェクトは2種類存在します。両者の違いは、
- Native datetime:
- タイムゾーン情報を「持たない」日時オブジェクト
- Aware datetime:
- タイムゾーン情報を「持つ」日時オブジェクト
たったこれだけのことですが、基本的にはAware datetimeを使うほうが安全でしょう。
実行環境に依存しないdatetime.now()
まず、現在時刻を取得する方法であるdatetime.now()
は、実行環境のシステム時刻に依存します。したがって、サーバーを借りてシステムを稼働させるときには、タイムゾーン情報を指定して現在時刻を取得するなどの対応が必要です。
from datetime import datetime, timezone
import time
from zoneinfo import ZoneInfo
# システム設定のタイムゾーン情報を取得する
print(time.tzname)
# 環境依存
print(datetime.now())
# UTCで固定して取得する
print(datetime.now(timezone.utc))
print(datetime.now().astimezone(timezone.utc))
# 東京時間で取得する
print(datetime.now(ZoneInfo("Asia/Tokyo")))
# ('東京 (標準時)', '東京 (夏時間)')
# 2024-12-26 22:10:00.000000
# 2024-12-26 13:10:00.000000+00:00
# 2024-12-26 13:10:00.000000+00:00
# 2024-12-26 22:10:00.000000+09:00
文字列からパースする(タイムゾーン付き)
文字列から日時オブジェクトを作るとき、datetime.strptime()
を使うことが多いと思いますが、これによって生成されるのはNative datetimeです。
from datetime import datetime
from zoneinfo import ZoneInfo
JST_ZONEINFO = ZoneInfo("Asia/Tokyo")
date_str = "2024-12-26 15:00:00"
# Native
date_native = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
print(date_native)
# Aware
date_aware = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=JST_ZONEINFO)
print(date_aware)
# 2024-12-26 15:00:00
# 2024-12-26 15:00:00+09:00
文字列へフォーマットする(タイムゾーン付き)
こちらもstrftime()
を使うことが多いかと思いますが、避けるほうが良いかもしれません。
dt = datetime.now().replace(tzinfo=ZoneInfo("UTC"))
print(dt.strftime("%Y-%m-%d %H:%M:%S"))
print(dt.isoformat()) # ISO 8601
# 2024-12-26 22:10:00
# 2024-12-26T22:10:00.000000+00:00
ちなみに、例えばTypeScriptでこれをパースするには、以下のコードで十分です。(PythonでAPIを立てて、フロントエンドにTSを使うシーンを想定)標準化されたフォーマットに乗っかるほうが、機械可読性が高いので適切に使い分けたいです。
const isoString: string = "2024-12-26T15:00:00+09:00";
const date: Date = new Date(isoString);
.replace()
と.astimezone()
の違い
どちらも日時オブジェクトにタイムゾーン情報を付与する・書き換えるときに使うものですが、挙動が少し異なります。
replace()
は単純にtzinfo
(タイムゾーン)を書き換えるだけで、時差の計算は行われません。Native datetimeにタイムゾーン情報を付与するときに使うことが多いと思います。
astimezone()
はタイムゾーン情報を書き換えて、時差の計算も行います。Aware datetimeのみで利用可能です。
from datetime import datetime
from zoneinfo import ZoneInfo
# 現在時刻(UTC)を取得
utc_now = datetime.now(ZoneInfo("UTC"))
print(f"UTC: {utc_now}")
# replace()
wrong_tokyo = utc_now.replace(tzinfo=ZoneInfo("Asia/Tokyo"))
print(f"replace: {wrong_tokyo}")
# astimezone()
correct_tokyo = utc_now.astimezone(ZoneInfo("Asia/Tokyo"))
print(f"astimezone: {correct_tokyo}")
# UTC: 2024-12-26 12:10:00.000000+00:00
# replace: 2024-12-26 12:10:00.000000+09:00
# astimezone: 2024-12-26 21:10:00.000000+09:00
時刻比較における注意点
また、時刻を比較するときにAwareとNativeで異なる場合にエラーが発生します。こちらも頭の片隅においておくと良いかもしれません。
print(datetime.now() > datetime.now())
print(datetime.now(timezone.utc) > datetime.now(timezone.utc))
print(datetime.now() > datetime.now(timezone.utc))
# False
# False
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# Cell In[6], line 3
# 1 print(datetime.now() > datetime.now())
# 2 print(datetime.now(timezone.utc) > datetime.now(timezone.utc))
# ----> 3 print(datetime.now() > datetime.now(timezone.utc))
#
# TypeError: can't compare offset-naive and offset-aware datetimes
テストにおけるタイムゾーンの指定
テストコードでもタイムゾーンの固定をする場合には、freezegun
を使います。
インストール:pip install freezegun
import unittest
from datetime import datetime
from freezegun import freeze_time
from zoneinfo import ZoneInfo
class TestDateTime(unittest.TestCase):
# クラスメソッドに対して
@freeze_time("2024-12-26 15:00:00", tz_offset=9)
def test_fixed_time(self):
current = datetime.now(ZoneInfo("Asia/Tokyo"))
self.assertEqual(current.hour, 15)
self.assertEqual(current.minute, 0)
# withステートメントを使用する場合
def test_with_freeze(self):
with freeze_time("2024-12-26 15:00:00", tz_offset=9):
current = datetime.now(ZoneInfo("Asia/Tokyo"))
self.assertEqual(current.hour, 15)
おわりに
Webアプリを開発するにあたって、「開発環境ではJST(日本標準時)だが、サーバー環境はUTC(世界協定時)で動いている」といった問題がしばしば発生します。さらに、実装段階では意識していないとタイムゾーンがごちゃごちゃになってしまうとか、取り扱いを間違えると深刻な不具合になるとか、厄介さがあります。
毎回これで悩まされているので、この内容を意識して実装するようにしたいです。