ファイルのタイムスタンプをチェックしたり、プログラムの前回実行日時からの差分データを抽出したり等々、日付や時刻の操作は欠かせないだろう。今回も淡々と地味に。
開発環境:
Python 3.9.2
参考情報:
現在日時の取得。
from datetime import datetime, timedelta, timezone
justnow = datetime.now()
>>> justnow
datetime.datetime(2021, 6, 10, 22, 24, 57, 156108)
>>>
>>> print(justnow)
2021-06-10 22:24:57.156108
ファイル名に日時を含みたくなることも良くある。
name = 'test'
filename = f'{name}_{justnow:%Y%m%d_%H%M%S}.txt'
>>> filename
'test_20210610_222457.txt'
下記は、ログで良くあるフォーマットだ。生成されるのは、もちろんstr型だ。
justnow.isoformat()
>>> justnow.isoformat()
'2021-06-10T22:24:57.156108'
timedeltaで足したり引いたり。
>>> (justnow + timedelta(days = 11)).isoformat()
'2021-06-21T22:24:57.156108'
>>>
>>> justnow - timedelta(weeks = 3)
datetime.datetime(2021, 5, 20, 22, 24, 57, 156108)
>>>
DBなどのレコードに書き込まれた日時情報を取り出したらエポックタイムになっていることもある。datetime型からエポックタイムへの変換と、その逆の操作だ。
>>> type(justnow)
<class 'datetime.datetime'>
>>> epochtime = datetime.timestamp(justnow)
>>> epochtime
1623331497.156108
>>> type(epochtime)
<class 'float'>
>>>
>>> datetime.fromtimestamp(epochtime)
datetime.datetime(2021, 6, 10, 22, 24, 57, 156108)
エポックタイムからdatetimeへの変換する際に、エポックタイムがstr型になっていたら書式に合わせ、int型なりfloat型なり適切な型を選択しよう。
>>> epochstring = '1623325116'
>>> datetime.fromtimestamp(epochstring) # エラーになる
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type str)
>>>
>>> datetime.fromtimestamp(int(epochstring))
datetime.datetime(2021, 6, 10, 20, 38, 36)
下記のようなstr型からdatetime型への変換も日々の営みだ。
lastseen = '2021/06/21 18:43:11'
lastseen_conv = datetime.strptime(lastseen, '%Y/%m/%d %H:%M:%S')
>>> lastseen_conv
datetime.datetime(2021, 6, 21, 18, 43, 11)
端末や管理用ワークステーションなどで使うお役立ちツールという位置付けならば上記の操作で構わないと思う。
しかし、マイクロサービスであちこちのサーバを行き来するうちに何れかに設定ミスがあったり、バックエンドのサーバが諸事情でUTCで稼働していたりすると意図せずタイムゾーン跨った処理も発生することもある。つまりタイムゾーンが分からない日時情報をサーバ間で受け渡ししていると、構成によっては危険だと言える可能性を孕んでいる。開発環境では問題なくともデプロイ先でやられることもある。
jst = timezone(timedelta(hours=+9), 'JST')
justnow_tz = datetime.now(jst) # これはタイムゾーンを指定しているからaware
>>> justnow_tz.strftime('%Y%m%d%H%M%S')
'20210610222457'
>>> justnow.strftime('%Y%m%d%H%M%S')
'20210610222457'
>>> justnow_tz.tzinfo
datetime.timezone(datetime.timedelta(seconds=32400), 'JST') # 指定したタイムゾーンが出力される
>>>
>>> justnow.tzinfo # これはタイムゾーンを指定していないからnaive
>>> # 何もない!
>>>
>>> justnow_tz.utcoffset()
datetime.timedelta(seconds=32400) # オフセットを取得できる
>>>
>>> justnow.utcoffset()
>>> # 何もない!
>>>
tzinfoやutcoffsetで出力を得られるdatetimeオブジェクトをaware、そうではないdatetimeオブジェクトをnaiveと呼ぶ。言い換えるとnaiveは処理する側の解釈に委ねられ、awareは厳密であるということだ。
>>> jst = timezone(timedelta(hours=+9), 'JST')
>>> justnow_tz = datetime.now(jst) # これはaware
>>> justnow = datetime.now() # これはnaive
>>> milestone = datetime.now() + timedelta(days = 9) # これはnaive
>>>
>>> milestone - justnow # naiveとnaive`
datetime.timedelta(days=9, microseconds=2547)
>>> milestone - justnow_tz # naiveとaware
Traceback (most recent call last): # 異なると減算できない
File "<stdin>", line 1, in <module>
TypeError: can't subtract offset-naive and offset-aware datetimes
もちろん、DBはUTCでフロントエンドをローカルタイムという設計思想はありだと思う。このような場合には、awareが適している。フロントエンド処理でタイムゾーンを指定するのだ。
※timeオブジェクトもawareとnaiveがある
まとめ:
datetime,timeを扱う際はawareとnaiveを意識しよう
複数人で開発する際は仕様書にawareかnaiveか明記してあると安心安全