Skip to content

Instantly share code, notes, and snippets.

@Enchan1207
Last active August 25, 2021 00:01
Show Gist options
  • Save Enchan1207/8027e730caf486428843061a231f3418 to your computer and use it in GitHub Desktop.
Save Enchan1207/8027e730caf486428843061a231f3418 to your computer and use it in GitHub Desktop.
pythonのdatetimeとタイムゾーンとpytzの話

pythonのdatetimeとタイムゾーンとpytzの話

datetimeの概要

pythonで日時を扱いたい時、標準のdatetimeモジュールを使うことが多いかと思います。

>>> from datetime import datetime
>>> datetime.now()
datetime.datetime(2021, 8, 24, 22, 52, 51, 0)

普通に使うならこれで困りません(UTCになっているということもなく、コンピュータの設定時刻と同じものを出力してくれます)。

では、 タイムゾーンを表示してくれ、 となったら…?

strftimeがあるじゃないか

「なあんだ、そんなの datetime.strftime() でできるじゃないか」と思うかもしれません。 確かに実装そのものは可能です:

>>> datetime.now().strftime('%Y-%m-%dT%H:%M:%S%z')
'2021-08-24T22:55:39'

…おや? %z を入れているのにタイムゾーンが表示されません。

そう、datetime.now()タイムゾーンの情報を持たないのです。 (これをnaiveオブジェクトと呼ぶようです。反対にタイムゾーンを持つdatetimeはawareオブジェクトと呼ばれます)

コードを書く側はJSTがUTC+09:00であることはわかりますが、それをどうやってdatetimeに 載せればいいのでしょうか。

当然こうする訳ではありません:

>>> datetime.now().strftime('%Y-%m-%dT%H:%M:%S+09:00')
'2021-08-24T23:00:08+09:00'

pytzによるタイムゾーンの設定

「python タイムゾーン 設定」などのワードでググると、pytzによる実装例が多く見つかります。

>>> import pytz
>>> datetime.now(tz=pytz.timezone('Asia/Tokyo'))
datetime.datetime(2021, 8, 24, 23, 1, 48, 818381, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

ISO 8601形式の表現が欲しいときは、

>>> datetime.now(tz=pytz.timezone('Asia/Tokyo')).isoformat()
'2021-08-24T23:02:48.502160+09:00'
>>> datetime.now(tz=pytz.timezone('Asia/Tokyo')).isoformat(timespec='seconds')
'2021-08-24T23:03:38+09:00'

datetime.isoformat()で取得することができます。非常に簡単です。

…では、 pytz.timezone(tzstring) は何を返しているのでしょうか。

datetime.tzinfo によるタイムゾーン表現

まず pytz.timezone() の中身を見てみましょう。

>>> pytz.timezone('Asia/Tokyo')
<DstTzInfo 'Asia/Tokyo' LMT+9:19:00 STD>
>>> type(pytz.timezone('Asia/Tokyo'))
<class 'pytz.tzfile.Asia/Tokyo'>

pytz.tzfile.Asia/Tokyo が返っているようです。 datetime.now()の引数tzが受け取るのは Optional[_tzinfo] であり、 datetimeのソースから_tzinfotzinfoつまりdatetime.tzinfoであることがわかります。

つまり issubclass(type(pytz.timezone('Asia/Tokyo')), datetime.tzinfo) == Trueであるはずです:

from datetime import tzinfo
>>> issubclass(type(pytz.timezone('Asia/Tokyo')), tzinfo)
True

ドキュメントを見ると、

class datetime.tzinfo このクラスは抽象基底クラスで、直接インスタンス化すべきでないことを意味します。 tzinfo のサブクラスを定義し、ある特定のタイムゾーンに関する情報を保持するようにしてください。

tzinfo (の具体的なサブクラス) のインスタンスは datetime および time オブジェクトのコンストラクタに渡すことができます。後者のオブジェクトでは、データ属性をローカル時刻におけるものとして見ており、 tzinfo オブジェクトはローカル時刻の UTC からのオフセット、タイムゾーンの名前、 DST オフセットを、渡された日付および時刻オブジェクトからの相対で示すためのメソッドを提供します。

ということなので、

  • datetime.datetimeにタイムゾーンを渡すには datetime.tzinfo を渡せばよい
  • しかし datetime.tzinfo をそのまま渡すことはできず、サブクラス化する必要がある
  • pytzは各地のタイムゾーンをdatetime.tzinfo形式で返してくれるライブラリ

ということがわかります。

自作tzinfo

つまりpytzが特別なライブラリというわけではなく、単純にtzinfoを返すクラスを自分で作れれば タイムゾーンを自由に定義できるということになります。

作ってみましょう。

from datetime import datetime, timedelta, tzinfo
from typing import Optional

class NewGeneration(tzinfo):

    def utcoffset(self, dt: Optional[datetime]) -> Optional[timedelta]:
        return timedelta(hours=23)

    def dst(self, dt: Optional[datetime]) -> Optional[timedelta]:
        return timedelta(0)

    def tzname(self, dt: Optional[datetime]) -> Optional[str]:
        return "NEW☆GENERATION"

できました。 UTC+23:00とかいう狂気のタイムゾーン です。どこに位置してるんだ。

これを使ってdatetimeを初期化すると…

>>> date_newzone = datetime.now(tz=NewGeneration())
>>> date_newzone.tzinfo.tzname(None)
'NEW☆GENERATION'
>>> date_newzone.isoformat(timespec='seconds')
'2021-08-25T13:23:30+23:00'

自作タイムゾーンがdatetimeにより保持されていることがわかります。

結論

標準のdatetime.timezoneモジュールを使うのが一番早いと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment