⏳
Python の datetime よくわからん人向けのなにか
この記事では Python の datetime
について取り扱います。
import datetime
忙しい人向けのまとめ
- なるべく aware (タイムゾーン付き)日時を扱う。
-
datetime.datetime.now()
にタイムゾーンを入れ忘れないようにする。 - ビジネスロジック上で naive と aware 日時を混ぜないようにする。
- リポジトリなど生データに近い部分で変換しておく。
- 日付情報だけを扱いたい場合は
date
を使う。- 記録媒体によっては日付をサポートしないこともある。
-
- シリアライズ・デシリアライズする場合:
- naive な日時でしか保存できない場合は UTC とする。
- 代替案として Unix Timestamp や ISO8601 形式が利用できないか検討する。
- Unix Timestamp だと比較がしやすい。
- システムがタイムスタンプを 64bit で扱うか確認が必要。
- Unix Timestamp だと比較がしやすい。
- もちろん例外はあるので適宜いい感じにする(重要)。
naive 日時と aware 日時
- Pythonの日時は二種類あります。
-
naive 日時。 時差情報
tzinfo
を持ちません。 -
aware 日時。 時差情報
tzinfo
を持ちます。
-
naive 日時。 時差情報
-
datetime
のインスタンスdt
が以下のふたつを満たす場合に aware 日時となります。-
dt.tzinfo
がNone
でない (タイムゾーン情報が存在する) -
dt.tzinfo.utcoffset(dt)
がNone
を返さない (タイムゾーンのオフセットが未定義でない)
-
def is_aware(dt: datetime.datetime):
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
インスタンスの生成
日時を指定してインスタンスを生成する
- 少なくとも
year
,month
,day
が指定されていればインスタンスを生成できます。省略された引数は0
を指定したものとして扱われます。 - JavaScript では1月32日のような存在しない日時を指定しても2月1日に正規化されますが、Pythonでは
ValueError
となります。
# 指定した日時情報でローカル日時を生成する (naive)
datetime.datetime(2022, 1, 1, 0, 0, 0)
# 指定した日時情報でUTC日時を生成する (aware)
datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
現在時刻を持つインスタンスを生成する
-
now()
は naive な現在日時を返します。 -
utcnow()
も同じく naive なUTC現在日時を返します。- システムのタイムゾーンと異なることがあるので、naive 日時でUTCを扱うのはあまりよくない考えです。
- システムのタイムゾーンが特定のタイムゾーンであることを想定しないでください。
-
now()
やutcnow()
を短い間に呼び出すと同じ値を得ることがあります。- 特に Windows のような日時分解能が低い環境で発生します。
- 毎回異なる日時が得られるといった想定をしないでください。
# ローカル日時を取得する (naive)
datetime.datetime.now()
# UTC日時を取得する (naive)
datetime.datetime.utcnow()
# 指定したタイムゾーンでの日時を取得する (aware)
datetime.datetime.now(datetime.timezone.utc)
扱える年の最小値と最大値
- 西暦1年から9999年まで扱えます。
datetime.MINYEAR # 1
datetime.MAXYEAR # 9999
- この範囲を超える西暦を扱いたい場合には天文ライブラリの
astropy.time
などがあります。 - システム上の無効な値として
0001-01-01T00:00:00
や9999-12-31T23:59:59
を使用しないでください。-
tzinfo
によってこの範囲を超えることがあります。 - 記録媒体によってはこの日時をサポートしないことがあります。
-
1970-01-01T00:00:00Z
未満の日時は Unix Timestamp が負数になります。
-
日時の一部を変更する
-
replace()
を使用すると、指定した要素だけ書き換えた新しいインスタンスを生成できます。
a = datetime.datetime(2022, 1, 1, 0, 0, 0)
a.replace(day=5)
# datetime.datetime(2022, 1, 5, 0, 0)
日時情報のシリアライズ
Unix Timestamp との相互変換
-
timestamp()
はシステムのタイムゾーンまたはtzinfo
に基づいて Unix Timestamp を返します。 - naive 日時の場合、システムのタイムゾーンと一致している限り正しく変換されます。
-
utcnow()
で取得した naive 日時を Unix Timestamp に変換するとずれます。
-
a = datetime.datetime(2022, 1, 1, 0, 0, 0)
b = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
a.timestamp() # 1640962800.0
b.timestamp() # 1640995200.0
-
fromtimestamp()
はシステムのタイムゾーンまたはtz
に基づいて日時が復元されます。 - naive, aware 日時ともに正しく変換されます。
a = datetime.datetime.fromtimestamp(1640995200)
b = datetime.datetime.fromtimestamp(1640995200, tz=datetime.timezone.utc)
a # datetime.datetime(2022, 1, 1, 9, 0)
b # datetime.datetime(2022, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
- 現在の Unix Timestamp だけが欲しいのであれば、
time.time()
も使用できます。
ISO8601 形式との相互変換
-
isoformat()
で ISO8601 形式に変換できます。- naive 日時でも動作しますが、時差情報が付加されません。
a = datetime.datetime(2022, 1, 1, 0, 0, 0)
b = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
a.isoformat() # '2022-01-01T00:00:00'
b.isoformat() # '2022-01-01T00:00:00+00:00'
-
fromisoformat()
はisoformat()
の逆関数です。- 逆関数としての役割しか持たないので、例えば時差情報が
+00:00
のかわりにZ
になっていると動作しません。 - 高度な変換を行いたい場合にはサードパーティ製の
dateutil
を利用可能です。
- 逆関数としての役割しか持たないので、例えば時差情報が
- 時差情報があれば aware, なければ naive 日時が返却されます。
datetime.datetime.fromisoformat('2022-01-01T00:00:00')
# datetime.datetime(2022, 1, 1, 0, 0)
datetime.datetime.fromisoformat('2022-01-01T00:00:00+00:00')
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
datetime.datetime.fromisoformat('2022-01-01T00:00:00+09:00')
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400)))
自由な形式での相互変換
-
strftime()
で日時から文字列に、strptime()
で文字列から日時に変換できます。- ISO8601 の相互変換をするなら前述の
dateutil
を使用したほうがいいかもしれません。 -
%z
指定子が与えられなかった場合、strptime()
は naive 日時を返します。
- ISO8601 の相互変換をするなら前述の
# 日時から年月日表記に変換する
a = datetime.datetime(2022, 1, 1)
a.strftime('%Y年%m月%d日') # '2022年01月01日'
# 年月日表記から日時に変換する
datetime.datetime.strptime('2022年01月01日', '%Y年%m月%d日')
# datetime.datetime(2022, 1, 1, 0, 0)
日時の計算
日時に時間を足す・引く
- timedelta オブジェクトを使います。
-
days
,seconds
,microseconds
の範囲で正規化されます。
datetime.timedelta(days=5) # datetime.timedelta(days=5)
datetime.timedelta(seconds=86401) # datetime.timedelta(days=1, seconds=1)
- 負の時間を与えることもできますが、正の時間を作って引く方が見栄えがいいかもしれません。
a = datetime.datetime(2022, 1, 1)
a + datetime.timedelta(days=1) # datetime.datetime(2022, 1, 2, 0, 0)
a - datetime.timedelta(days=1) # datetime.datetime(2021, 12, 31, 0, 0)
a - datetime.timedelta(days=-1) # datetime.datetime(2022, 1, 2, 0, 0)
日時の差
-
datetime
オブジェクト同士を引くとtimedelta
オブジェクトになります。 -
timedelta
が表す時間の総秒数を得るにはtotal_seconds()
を使います。
a = datetime.datetime(2022, 1, 5)
b = datetime.datetime(2022, 1, 1)
a - b # datetime.timedelta(days=4)
(a - b).total_seconds() # 345600.0
大小比較
-
==
,!=
,<
,<=
,>
,>=
が利用可能です。 - naive と aware 日時を比較することはできません。
- aware 日時は時差情報に基づいて絶対的な時間で判定するので、次のふたつの日時は同じになります。
a = datetime.datetime.fromisoformat('2022-01-01T00:00:00+00:00')
b = datetime.datetime.fromisoformat('2022-01-01T09:00:00+09:00')
a == b # True
タイムゾーン
標準ライブラリに用意されているタイムゾーン
- UTCタイムゾーンの
datetime.timezone.utc
だけが用意されています。-
timezone
はtzinfo
クラスのサブクラスです。
-
- それ以外のタイムゾーンは自分で定義するか、
dateutil.tz
を使用してください。
a = datetime.datetime(2022, 1, 1)
datetime.timezone.utc.utcoffset(a) # datetime.timedelta(0)
datetime.timezone.utc.tzname(a) # UTC
タイムゾーンの作成
- Python 3.9 からは zoneinfo が使えます。
import zoneinfo
zoneinfo.ZoneInfo('Asia/Tokyo')
- Python 3.8 以下では…
-
tzinfo
クラスのサブクラスを定義します。 -
utcoffset()
,tzname()
,dst()
の定義が必須です。
-
# Asia/Tokyo
class JST(datetime.tzinfo):
def __repr__(self):
return self.tzname(self)
def utcoffset(self, dt):
# ローカル時刻とUTCの差分に等しいtimedeltaを返す
return datetime.timedelta(hours=9)
def tzname(self, dt):
# タイムゾーン名を返す
return 'Asia/Tokyo'
def dst(self, dt):
# 夏時間を返す。有効でない場合はtimedelta(0)を返す
return datetime.timedelta(0)
とにかく日本時間で aware 日時を得たい場合
# Python 3.8 まで
datetime.datetime.now(JST())
# Python 3.9 から
datetime.datetime.now(zoneinfo.ZoneInfo('Asia/Tokyo'))
タイムゾーンの変更
日時を追従させない場合
-
replace()
を使ったタイムゾーンの変更ではtzinfo
だけが変更され、日時は変更されません。
# naive日時にタイムゾーンを付加する
a = datetime.datetime(2022, 1, 1, 0, 0, 0)
a.replace(tzinfo=JST())
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=Asia/Tokyo)
# aware日時のタイムゾーンを変更する
b = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
b.replace(tzinfo=JST())
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=Asia/Tokyo)
# aware日時のタイムゾーンを削除する
c = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=JST())
c.replace(tzinfo=None)
# datetime.datetime(2022, 1, 1, 0, 0)
日時を追従させる場合
-
astimezone()
によるタイムゾーンの変更は与えられたtzinfo
に追従するように日時が正規化されます。- naive 日時はシステムのタイムゾーンでの日時として解釈されます。
# naive日時のタイムゾーンを変更する(日本時間なので変更なし)
a = datetime.datetime(2022, 1, 1, 0, 0, 0)
a.astimezone(tz=JST())
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=Asia/Tokyo)
# aware日時のタイムゾーンを変更する
b = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
b.astimezone(tz=JST())
# datetime.datetime(2022, 1, 1, 9, 0, tzinfo=Asia/Tokyo)
# システムのタイムゾーンに変更する
c = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=JST())
c.astimezone(tz=None)
# datetime.datetime(2022, 1, 1, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))
naive から aware への変換 (現地時間 → 絶対時間)
-
replace()
で naive 日時が想定しているタイムゾーンを設定する。
aware から naive への変換 (絶対時間 → 現地時間)
-
astimezone()
で得たい日時にしてからreplace()
でタイムゾーンを削除する。
# awareなUTC日時からnaiveな日本時間を得る
a = datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
b = a.astimezone(tz=JST()).replace(tzinfo=None)
# datetime.datetime(2022, 1, 1, 9, 0)
aware から aware への変換 (時差の変更)
-
astimezone()
でタイムゾーンを変更する。
SQLite3 を使用する際の注意点
-
SQLite3 には
TIMESTAMP
型が存在しません。- 必然的に
INTEGER
やNUMERIC
,TEXT
型で保存することになります。 - ユーザー定義型とその変換関数を定義することもできます。
- 必然的に
datetime をシリアライズ・デシリアライズする
- 標準で
date
とdatetime
用に変換関数が用意されています。-
connect()
のdetect_types
にPARSE_DECLTYPES
パラメータを与えると、CREATE 文でDATE
(date
) またはTIMESTAMP
(datetime
) 型として定義したカラムの読み書きで相互変換が行われるようになります。 -
PARSE_COLNAMES
パラメータを与える方法もありますが、通常利用では必要ないでしょう。
-
with sqlite3.connect('a.db', detect_types=sqlite3.PARSE_DECLTYPES) as con:
cur = con.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS test (
id INTEGER PRIMARY KEY,
created_at TIMESTAMP
)
''')
- ただしこの変換関数は aware な日時には対応していません。
- は?🙄
- この問題を解決するためにユーザー定義の変換関数を用意します。
- データベース上の日時をUnix Timestamp で扱う場合は日時の大小比較が簡単です。また、ISO8601 形式で扱う場合は人間にとって読みやすいです。今回は Unix Timestamp で扱う例を示します。
- ユーザー定義の変換関数でもともとの変換関数を上書きできます。
# Adapter: Python→SQLite3
sqlite3.register_adapter(
datetime.datetime,
lambda x: x.timestamp()
)
# Converter: SQLite3→Python
sqlite3.register_converter(
'TIMESTAMP',
lambda x: datetime.datetime.fromtimestamp(
float(x), # SQLite3からは必ずbytesオブジェクトが渡されます
tz=datetime.timezone.utc
)
)
with sqlite3.connect('a.db', detect_types=sqlite3.PARSE_DECLTYPES) as con:
pass
おわり🌱
Discussion