Python、pytzの詳細

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - サードパーティ製ライブラリ
2018年12月4日19:02に更新(約9日前)
2018年12月3日23:47に作成(約10日前)

概要

Pythonではdatetimeモジュールとpytzを使うことでタイムゾーンに関する処理が行えます。

Pythonで、pytzを使ったタイムゾーンの変換Djangoで、タイムゾーンの変換でタイムゾーンに関する説明を行ってきましたが、今回はもう少しpytzdatetimeのタイムゾーン関連の詳細に踏み込んでいきます。

tzinfoオブジェクトとは

datetimeモジュールはタイムゾーンに関する具体的な処理はしませんが、それを行うためのインターフェースは定義しており、tzinfoという抽象クラスを提供しています。このtzinfoサブクラスに、タイムゾーン関連の具体的な処理を任せることになります。

このtzinfoが何をするかといいますと、例えばdt.astimezone(tzinfo)とするとタイムゾーンを変更することができますが、内部的な処理としてはdatetimeをUTCに変換し、tzinfo.fromutc()を呼び出すという処理を行います。

tzinfo.fromutc()は、UTC時間を受け取り、タイムゾーンを考慮した正しいdatetimeオブジェクトを返すメソッドとして実装されていなければなりません。

tzinfoオブジェクトを作ってみる

試しに作ってみましょう。架空の国を作り、架空のタイムゾーンを作ってみましょう。国の名前は「ネオサイタマ」で、UTC+10:00 とします。ネオサイタマの深夜1時は日本時間で0時、UTCで15時です。

以下のような感じで、tzinfoを継承したクラスを作り、幾つかのメソッドを上書きします。

from datetime import tzinfo, datetime, timedelta


class NeoSaitama(tzinfo):
    _utcoffset = timedelta(hours=10)

    def fromutc(self, utc_dt):
        neo_saitama_dt = utc_dt + self._utcoffset
        return neo_saitama_dt

    def utcoffset(self, dt):
        return self._utcoffset

実際に動くか試したあとに、中身のコードを説明ていきます。

datetime.now(tz=tzinfo)を試す

ネオサイタマでの現在時間を取得してみましょう。

neo_saitama_tz = NeoSaitama()
now = datetime.now(tz=neo_saitama_tz)
print(now)

まず、tzinfoオブジェクトを作成します。pytzでいうと、pytz.timezone('Asia/Tokyo')の部分ですね。

neo_saitama_tz = NeoSaitama()

現在時間を取得しています。tz引数にtznfoオブジェクトを渡します。

now = datetime.now(tz=neo_saitama_tz)

出力結果は以下のようになります。

2018-12-03 23:18:03.074036+10:00

私は実在する人物で、実在する日本にすんています。日本の現在時間は22:18です。ネオサイタマとの時差は1時間なので、23:18であっていますね。

それではNeoSaitamaを説明していきます。実際はもう少し実装すべきメソッドもあるのですが、今回の例ならfromutc()utcoffset()の2つで十分です。

まず重要なのがutcoffset()です。これは、このタイムゾーンがUTCとどれぐらい時差をあるか?を表すものです。差はtimedelta型として表します。今回は10時間ですね。

    _utcoffset = timedelta(hours=10)
    ...
    ...
    def utcoffset(self, dt):
        return self._utcoffset

メソッドを呼ぶたびにtimedelta()を作るのはちょっと嫌なので、_utcoffsetというクラス属性にしています。

fromutc()も重要です。UTCに変換されたdatetimオブジェクトを受け取って、タイムゾーンに合わせたdatetimeオブジェクトを返します。

    def fromutc(self, utc_dt):
        neo_saitama_dt = utc_dt + self._utcoffset
        return neo_saitama_dt

datetime.astimezone()datetime.now()でこのfromutc()が呼び出されます。UTCに変換したdatetimeオブジェクトが引数です。今回の例ならば、2018-12-03 13:18:03といったdatetimeオブジェクトですね。

これに、ネオサイタマのUTCとの時差である10時間を足せばネオサイタマ時間に変換できます。やってることはシンプルですね。

datetime.astimezone(tz=tzinfo)を試す

日本の現在時間を取得し、astimezoneでネオサイタマに変換してみましょう。

import pytz

neo_saitama_tz = NeoSaitama()
jst = pytz.timezone('Asia/Tokyo')
jst_now = datetime.now(tz=jst)
neo_saitama_now = jst_now.astimezone(tz=neo_saitama_tz)
print(neo_saitama_now)

こちらも問題なしですね。

2018-12-03 23:30:03.074036+10:00

ネオサイタマから他のタイムゾーンへの変換も問題なく動作します。

import pytz

neo_saitama_tz = NeoSaitama()
jst = pytz.timezone('Asia/Tokyo')
neo_saitama_now = datetime.now(tz=neo_saitama_tz)
jst_now = neo_saitama_now.astimezone(tz=jst)
print(jst_now)

datetime.now(tz=hoge)やdt.astimezone(tz=hofe)の詳細

tzinfoサブクラスを作りいくつかのメソッドを上書きすると、ちゃんとタイムゾーン関連の処理ができることを確認しました。datetime.now()dt.astimezone()の内部で、私たちが作ったtzinfoのutcoffset()fromutc()が呼び出されるためですね。

では、実際にどのように呼び出されているかもちらっと紹介していきます。

dt.astimezone(tz)がやっているのは以下のような処理です。

def astimezone(self, tz):
    mytz = self.tzinfo
    myoffset = mytz.utcoffset(self)
    utc_dt = self - myoffset
    utc_dt = utc_dt.replace(tzinfo=tz)
    return tz.fromutc(utc_dt)

これはdatetimeクラスのメソッドなので、引数selfはdatetimeオブジェクトそのものです。selfが日本時間での2018-12-03 23:18:03で、tzがネオサイタマだったとしましょう。つまり日本の現在時間をネオサイタマに変換する例とします。

mytz = self.tzinfoで、日本のtzinfoオブジェクトが取得されます。

offset = mytz.utcoffset(self)は日本タイムゾーンのUTCとの時差なので、9時間ですね。

utc_dt = self - myoffsetは、23:18から9時間引き、UTC時間なdatetimeオブジェクトを作ります。tzinfoは必ずUTCとの時差を持っている(utcoffset())ので、UTCへの変換は簡単に行えますね。

utc_dt = utc_dt.replace(tzinfo=tz)は、そのdatetimeオブジェクトのtzinfoを更新しています。UTC時間に変換したものをネオタイサマに変換したいのですが、日本のtzinfoがくっついているままだと紛らわしいので設定します。実際、これをしないとpytzはエラーになります。

そして、tz.fromutc(utc_dt)とすることでネオサイタマのfromutc()でネオサイタマ時間に変換します。

dt.astimezone(tz)を説明したので、datetime.now(tz=hofe)にも触れておきましょう。

datetime.now()にtz引数を指定すると、概ね以下のような処理をします。

def now(tz):
    t = time.time()
    y, m, d, hh, mm, ss, weekday, jday, dst = time.gmtime(t)
    dt = datetime(y, m, d, hh, mm, ss, tzinfo=tz)
    aware_dt = tz.fromutc(dt)
    return aware_dt

now()関数がdatetime.now(tz=tzinfo)の簡易版です。実際はもう少し細かい調節をしていたりするのですが、処理としては概ね上のような感じです。

time.time()ですが、これはdatetimeではなくtimeモジュールのtime関数で、エポック からの秒数を返します。このエポックはコンピューターの世界でよく使われるもので、基本的にはUTCの1970年1月1日午前0時0分0秒から、今どのぐらいの秒数経っているかを返します。Unix時間とも言います。1543868924.2214994みたいな感じで、経過秒数を取得します。

time.gmtime()はこのエポックからの秒数を受け取って、年月日時間を返します。

年月日時間を取得できれば、datetime(y, m, d, hh, mm, ss)のようにしてdatetimeオブジェクトが作れますね。time.time()自体がUTCを基準にした現在時間を取得していますので、UTC時間でのdatetimeオブジェクトになります。

UTC時間で取得しているので、これは素直にtz.fromutc()に渡せます。これがdatetime.now(tz=tzinfo)の仕組みです。

おまけ1 datetime.now()がやっていること

ちなみにですが、datetime.now()で引数を指定しなかった場合は以下のような処理です。

def now():
    t = time.time()
    y, m, d, hh, mm, ss, weekday, jday, dst = time.localtime(t)
    local_dt = datetime(y, m, d, hh, mm, ss)
    return local_dt

time.localtime()は、ローカル時間を取得してくれます。お近くの時計が指している時間のことです。タイムゾーン関連の処理はせず、そのままdatetimeオブジェクトを返します。

おまけ2 datetime.utcnow()がやっていること

また、datetime.utcnow()という現在UTC時間でのdatetimeオブジェクトを取得するためのメソッドもあるのですが、その場合は以下のようなことをします。

def utcnow(tz):
    t = time.time()
    y, m, d, hh, mm, ss, weekday, jday, dst = time.gmtime(t)
    utc_dt = datetime(y, m, d, hh, mm, ss)
    return utc_dt

UTCから他時間に切り替える必要がない...utcnow()という名前の処理ですので。fromutc()でのタイムゾーン処理は不要です。

そのため、UTCでのdatetimeオブジェクトが欲しければ、datetime.now(tz=pytz.utc)よりもdatetime.utcnow().replace(tzinfo=pytz.utc)のほうが早いです。ただ注意なのは、utcnowではtzinfoオブジェクトの設定まではしないので、replace(tzinfo=pytz.utc)のようにtzinfo引数の設定が必要です。

datetime()とreplace()のtzinfo引数

datetime.now(tz=hoge)dt.astimezone(tz=hoge)としたときの動作はわかりました。tzinfoオブジェクトが使われ、fromutc()utcoffset()等のメソッドが呼び出されて時刻が修正されることもわかりました。

では、datetime(2018, 1, 1, tzinfo=hoge)replace(tzinfo=hoge)はどうでしょうか。結論から話すとこれらのtzinfo引数は使わないほうが無難です。

※あくまでtzinfo引数の話です。

説明しやすくするために、日本のtzinfoオブジェクトを作成し、reprでの出力結果を見てみましょう。

import pytz

jst = pytz.timezone('Asia/Tokyo')
print(repr(jst))

出力結果を見てみます。

<DstTzInfo 'Asia/Tokyo' LMT+9:19:00 STD>

日本のタイムゾーンはUTC+9:00:00のはずですが、+9:19:00になっています。

あまり知られていないことですが、日本は1887年までは東経139度を基準にしていました。現在は東経135度が基準ですね。経度が15度かわるごとに時差は1時間増え、5度くらいなら大体20分ということですね。

また、過去にサマータイムを導入していた時期もありました。

実際に見てみましょう。

import pytz

jst = pytz.timezone('Asia/Tokyo')
print(jst._tzinfos)

このように3つありますね。

{(datetime.timedelta(seconds=32400), datetime.timedelta(0), 'JST'): <DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>,
 (datetime.timedelta(seconds=33540), datetime.timedelta(0), 'LMT'): <DstTzInfo 'Asia/Tokyo' LMT+9:19:00 STD>,
 (datetime.timedelta(seconds=36000), datetime.timedelta(seconds=3600), 'JDT'): <DstTzInfo 'Asia/Tokyo' JDT+10:00:00 DST>}

jst = pytz.timezone('Asia/Tokyo')とインスタンス化した段階では日付がわかりませんので、適当に(正確には0番目の)タイムゾーンが使われます。日本の場合は、9:19:00のタイムゾーンがデフォルトで使われるということですね。

pytzのfromutc()は、UTCな日付からどのタイムゾーンが適切かを選び、それに合わせたdatetimeオブジェクトを返します。UTC時間が1850年とかなら、東経139度時代のタイムゾーンに当てはめて時刻が算出され、そのtzinfoが設定されるという訳です。非常に便利です。

しかし、datetime()replace()ではこのようなタイムゾーン関連の処理は全くしません。

datetimeモジュールのdatetimeはクラスなので、datetime.datetime()はただのインスタンス化です。やっているのは属性への代入処理ぐらいなもので、上手いことUTCに変換してfromutc()を呼び出すとか、localice()を呼び出すとか、そういうタイムゾーン処理てきなことはしません。replace()も新しいdatetimeインスタンスを作り直すだけなので、同じです。

from datetime import datetime
import pytz

jst = pytz.timezone('Asia/Tokyo')
dt = datetime(2019, 1, 1, tzinfo=jst)
# datetime(2019, 1, 1).replace(tzinfo=jst) も同じ
print(dt)

+09:00:00になってほしいところですが、+09:19:00になります。デフォルトで作られた09:19:00なタイムゾーンをそのまま設定しているためです。

2019-01-01 00:00:00+09:19

datetime()は、ある時点の日付時刻を作成するという機能です。これに関してはtzinfo.fromutc()は使えないので、tzinfo.localize()でタイムゾーンを設定する必要があります。注意なのはdatetime(2019, 1, 1, tzinfo=hoge)でtzinfoを設定してしまうと、localize()に渡せなくなります。localize()に渡せるのはnaiveな日付、つまりtzinfoがNoneのdatetimeオブジェクトです。

例外としては、UTCのようにゾーンが1種類しかない場合であれば使えます。UTCでの2019年1月1日のdatetimeオブジェクトが欲しい場合は以下のように書けます。

from datetime import datetime
import pytz

utc_aware_dt = datetime(2019, 1, 1, tzinfo=pytz.utc)
print(utc_aware_dt)

余談ですがpytzの昔のバージョンでは+09:19というゾーンは追加されておらず、+09:00がデフォルトでした。なのでtzinfo引数はそれなりに動作していました。しかし、高度な政治的理由などによって、どの国であろうと今後ゾーンが増える可能性はあります。それを踏まえると、UTC以外では使わないほうが無難です。09:19問題が起きます。

replace(tzinfo=hoge)に関しては、実用上だとUTCなdatetimeオブジェクトにUTCなtzinfoを設定するような場合ぐらいしか使いません。上でも出たdatetime.utcnow().replace(tzinfo=pytz.utc)ですね。それ以外で使うケースというのは、pytzやdatetimeモジュールのように開発者的な都合で触る場合ぐらいです。

所感

tzinfoのサブクラスを作っていて思いましたが、割と簡単に作れちゃうので、RPGやシミュレーションゲームで使ったりするとリアリティ的なものが出て面白いかもなーと感じました。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする