Pythonのタイムゾーンはpytzよりzoneinfoかdateutils.tzを使おう、という話
Python3.9では zoneinfoモジュールが追加され、標準ライブラリだけでタイムゾーンを使えるようになりました。
私もまだ使ったことはないのに無責任に勧めてしまいますが、今後の新しいプロジェクトでタイムゾーンを使うときは zoneinfo
を使いましょう。しかし、Python3.8以前を利用しているプロジェクトでは、いまだに pytz を使っている場合も多いのではないでしょうか?
pytz
以外にも、dateutilsパッケージのdateutil.tz もタイムゾーン機能を提供しており、pytz
よりもこちらのほうがおすすめです。
pytzの困ったところその1
Python3.6以降では、夏時間によって発生する曖昧な時刻の問題に対処するため、PEP 495 -- Local Time Disambiguation が導入されました。
夏時間を採用している地域では一日に同じ時間が2回発生する場合があり、PEP496ではこの問題に対処するために、 datetime
オブジェクトに1回目と2回めを区別する fold
という属性が追加されました。
pytz
は、Python3.6以前から存在しており、独自に曖昧な時刻を明確にする機能を持っています。しかし、datetime
オブジェクトのfold
属性には対応しておらず、正しくfold
を設定していても無視されてしまいます。
一方、zoneinfo
やdateutil.tz
はもちろん fold
属性をサポートしており、問題は発生しません。
pytzの困ったところその2
pytz
では、タイムゾーン情報を
tz = pytz.timezone("Asia/Tokyo")
のように取得します。pytz
から取得する tz
はPythonのdatetime
モジュールのtzinfo
クラスのサブクラスであり、一見、Pythonのタイムゾーン情報を表すtzinfo
として使えるように見えます。しかし、前述の夏時間対応のためなどに特殊な機能を持っており、tzinfo
と同じようには使えません。pytz
独自のインターフェースとして、Pythonのdatetime
モジュールの仕組みとは区別して利用する必要があります。
通常の、zoneinfo
やdateutil.tz
で取得したタイムゾーンオブジェクトは
datetime(2020, 1, 1, tzinfo=tz)
のようにdatetime()
オブジェクトの構築時に指定したり、
datetime(2020, 1, 1).replace(tzinfo=tz)
のようにreplace()
メソッドに指定したりできるのですが、pytz
の場合はこういった使い方をすると、つぎのように間違った結果になってしまいます。
>>> tz = pytz.timezone("Asia/Tokyo")
>>> print(datetime(2020, 1, 1).replace(tzinfo=tz))
2020-01-01 00:00:00+09:19
pytz
を使う場合は、tz.localize()
やdatetime.astimezone(tz)
などのメソッドだけを使うように気をつける必要があります。
pytzの困ったところその3
現在、日本の標準時(JST)は、協定世界時(UTC)と比べて9時間の時差があります。この9時間という時差は昔からずっと9時間だったわけではなく、大昔は9時間19分という半端な時間だったころもありました。
pytz
などが利用するタイムゾーンの情報を記録したデータベースには、こういった標準時の歴史的な変遷も記録されており、対象の日付によって、適切な時差(オフセット)がわかるようになっています。
pytz
は独自にタイムゾーンデータベースを持っていますが、それとは別にオペレーティングシステムもタイムゾーンデータベースを持っており、datetime
モジュールなどはこちらを利用します。このデータベースが、pytz
のデータベースと、プラットフォームのデータベースで食い違っている場合、正しい結果が得られない場合があります。
pytz
を使って、次のように1885年から1904年までの時間を変換してみましょう。
import pytz, datetime
tz = pytz.timezone("Asia/Tokyo")
for i in range(1885, 1905):
print(datetime.datetime(i,1,1,0,0,0).astimezone(tz))
この実行結果はプラットフォームとpytz
のバージョンによって異なりますが、手元のmacOS Catalinaでは次のようになります。
1885-01-01 00:00:01+09:19
1886-01-01 00:00:01+09:19
1887-01-01 00:00:01+09:19
1888-01-01 00:00:01+09:19
1889-01-01 00:19:00+09:19
1890-01-01 00:19:00+09:19
1891-01-01 00:19:00+09:19
1892-01-01 00:19:00+09:19
1893-01-01 00:19:00+09:19
1894-01-01 00:19:00+09:19
1895-01-01 00:19:00+09:19
1896-01-01 00:19:00+09:19
1897-01-01 00:19:00+09:19
1898-01-01 00:19:00+09:19
1899-01-01 00:19:00+09:19
1900-01-01 00:19:00+09:19
1901-01-01 00:19:00+09:19
1902-01-01 00:00:00+09:00
1903-01-01 00:00:00+09:00
1904-01-01 00:00:00+09:00
結果をみると、次のような感じになっています。
- 1888/1/1までは時刻は00:00:01で、時刻のオフセットは09:19
- 1889/1/1から1901/1/1までは時刻が00:19のオフセットが09:19
- それ以降は時刻が00:00:00でオフセットは09:00
なんだか不思議な結果になっています。これは pytz
のデータベースとシステムのデータベースで Asia/Tokyo
の内容が異なっているためです。macOSのデータベースでは1888年までのオフセットは9:18:59、それ以降は9:00となっているのですが、pytz
のデータベースでは1901年までが09:19で、それ以降が9:00となっています。
Pythonのdatatime.astimezone()
メソッドは、変換時に元の値をシステムのデータベースから取得したオフセットを使ってUTCに変換してから、その時刻をastimezone()
に指定した pytz
のタイムゾーン情報で変換します。どちらのデータベースが正しいのかは知りませんが、いずれにしろ、システムのデータベースとpytz
のデータベースに食い違いがあると、こんな感じで不思議な時間が出てきてしまうのです。
日本の場合はそれほどではありませんが、一つの国の中でも複数の時差と夏時間が入り交じる欧米諸国や、新興国などでは、もっと混乱した結果になってしまう場合もあるようです。zoneinfo
やdateutil.tz
では、(Windowsのようにデータベースを利用できない場合を除けば)プラットフォームのデータベースだけ使うため、何がほんとうに正しい結果なのかはともかく、すくなくともある程度の一貫性は確保された結果を得られます。
ということで
上記のpytz
の問題は、使い方を工夫すればある程度回避は可能です。しかし、現在はzoneinfo
やdateutil.tz
などが使えますので、少なくとも新しいプロジェクトではpytz
を使って苦労するより、回避が不要なzoneinfo
やdateutil.tz
を使ったほうが良いのではないでしょうか?
Discussion