Pythonでのタイムゾーンを扱う方法がわかりづらく感じたので、整理したメモです。
バージョン情報
- Python 3.8
- pytz 2019.3
datetimeクラス
同じクラスで以下の2種類のインスタンスがある。
- Aware
- 単に年月日時分秒の数値とタイムゾーンを持つ
- 絶対的な時間がわかり、Unixtimeへの変換もできる
- タイムゾーンが違えば同じ絶対的な時間でも別の値のインスタンスになる
- Naive
- 単に年月日時分秒の数値を持ち、タイムゾーンの情報がない
- Unixtimeなど絶対的な時間に変換するにはローカルのタイムゾーン情報への依存が必要
Aware and Naive Objects - datetime — Basic date and time types — Python 3.8.1rc1 documentation
※以下の実行例はすべて、環境変数がTZ=JST-9
となっている場合
現在時刻を取得する方法
from datetime import datetime
from datetime import timezone
now = datetime.now()
print(now) # => 日本時間のいま
print(now.tzinfo) # => None
# Naive
now = datetime.utcnow()
print(now) # => 日本時間から9時間引いた時刻
print(now.tzinfo) # => None
# Naive
now = datetime.now(timezone.utc)
print(now) # 9時間引いた時刻に "+00:00" という表記付き
print(now.tzinfo) # => UTC
# Aware
Unixtimeからdatetimeのインスタンスを取得する方法
from datetime import datetime
from datetime import timezone
unixtime = 1572764400
now = datetime.fromtimestamp(unixtime)
print(now) # => unixtimeが表す瞬間の日本時間での表記
print(now.tzinfo) # => None
# Naive
now = datetime.utcfromtimestamp(unixtime)
print(now) # 9時間引いた時刻 = unixtimeが表す瞬間のUTCでの表記
print(now.tzinfo) # => None
# Naive
now = datetime.fromtimestamp(unixtime, timezone.utc)
print(now) # 9時間引いた時刻に "+00:00" という表記付き
print(now.tzinfo)
# => UTC
# Aware
難しいことを言わずに手っ取り早く9時間ずらす方法
from datetime import datetime
from datetime import timedelta
now = datetime.now() # Naive
print(now) # => 日本時間のいま(タイムゾーン情報なし)
print(now - timedelta(hours = +9)) # => 9時間引いた時刻(タイムゾーン情報なし)
now = datetime.utcnow() # Naive
print(now) # => 9時間引いたいま(タイムゾーン情報なし)
print(now + timedelta(hours = +9)) # => 9時間足した日本時間(タイムゾーン情報なし)
タイムゾーンを表すクラス
tzinfoクラス
tzinfo
は以下の情報を持つ抽象クラス。
- タイムゾーンの名前
- UTCとの時間のオフセット値
- 夏時間におけるUTCとの時間のオフセット値
tzinfo - datetime — Basic date and time types — Python 3.8.1rc1 documentation
具象クラスとしてはtimezone
を使うか、pytz
を使う。pytz
は標準ライブラリにはなくてインストールが必要。
timezoneクラス
timezone
は固定のオフセット値を持ったtzinfo
の具象クラス。固定で+09:00のようなタイムゾーンとしては使えるが、夏時間など日時によってオフセット値が変わることを考慮できない。
pytzパッケージ
pytz
パッケージでは"Asia/Tokyo"
のような文字列からtzinfo
の具象クラスを生成できる。UTCとのオフセット値が日時によって変わることを考慮できる。
オフセット値が日時によって変わる例
- 北米、ヨーロッパ、オーストラリアなどでの夏時間
- 1995年にキリバスの一部で24時間時計を進めた
- 2014年にロシアで1時間時計を戻した
pytz
は標準のライブラリではないためインストールが必要。
$ pip install pytz
例
timezoneクラス
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import pytz
dt = datetime.utcfromtimestamp(1572764400)
print(dt)
# => 2019-11-03 07:00:00
# 1572764400は日本時間で3日16時
# timezone.utcはオフセット値が0、夏時間はない
utc = timezone.utc
print(utc.utcoffset(dt))
# => 0:00:00
print(utc.dst(dt))
# => None
# timezoneクラスのインスタンス
tz8 = timezone(timedelta(hours=-8)) # UTCより8時間遅れを表すtimezone
print(tz8.utcoffset(dt))
# => -1 day, 16:00:00
print(tz8.dst(dt))
# => None
# 日本時間はオフセット値が+9時間で夏時間はない
tokyo = pytz.timezone('Asia/Tokyo') # 日本時間
print(tokyo.utcoffset(dt))
# => 9:00:00
print(tokyo.dst(dt))
# => 0:00:00
# UTCの11月3日7時はアメリカ太平洋時間ではオフセット値が-8時間で夏時間ではない
lasvegas = pytz.timezone('America/Los_Angeles') # アメリカ太平洋時間
print(lasvegas.utcoffset(dt))
# => -1 day, 16:00:00
print(lasvegas.dst(dt))
# => 0:00:00
# その前日はアメリカ太平洋時間ではオフセット値が-7時間で夏時間である
print(lasvegas.utcoffset(dt - timedelta(days = 1)))
# => -1 day, 17:00:00
print(lasvegas.dst(dt - timedelta(days = 1)))
# => 1:00:00
datetimeのタイムゾーン変換
タイムゾーン情報のないNaiveなdatetimeインスタンスに対してastimezoneを使うとローカル環境のタイムゾーンから指定のタイムゾーンへ変換される。変換後はAwareなインスタンスになる。
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import pytz
dt = datetime.utcfromtimestamp(1572764400)
print(dt)
# => 2019-11-03 07:00:00
# 1572764400は日本時間で3日16時
# dtはUTCで3日7時のつもりで生成したインスタンスだが、
# タイムゾーン情報のないNaiveなインスタンスなので、
# 日本時間の3日7時とみなして指定のタイムゾーンに変換される。
tokyo = pytz.timezone('Asia/Tokyo') # 日本時間
print(dt.astimezone(tokyo))
# => 2019-11-03 07:00:00+09:00
# ローカルPC環境が日本時間なので、時間は変わらない
lasvegas = pytz.timezone('America/Los_Angeles') # アメリカ太平洋時間
print(dt.astimezone(lasvegas))
# => 2019-11-02 15:00:00-07:00
# 日本時間の11月3日7時はアメリカ太平洋時間では夏時間で日本よりも16時間遅れ
tz8 = timezone(timedelta(hours=-8)) # UTCより8時間遅れを表すtimezone
print(dt.astimezone(tz8))
# => 2019-11-02 14:00:00-08:00
タイムゾーン情報付きのAwareなdatetimeインスタンスに対してastimezoneを使うと元のタイムゾーンから指定のタイムゾーンへ変換される。変換後も引き続きAwareなインスタンスになる。
# dt.replace(tzinfo=utc) で年月日時分秒の数値を変えずにタイムゾーンの情報だけ付加している
print(dt.replace(tzinfo=utc).astimezone(tokyo))
# => 2019-11-03 16:00:00+09:00
print(dt.replace(tzinfo=utc).astimezone(lasvegas))
# => 2019-11-03 00:00:00-07:00
print(dt.replace(tzinfo=utc).astimezone(tz8))
# => 2019-11-02 23:00:00-08:00
夏時間が終わるタイミングでのUTCからアメリカ太平洋時間への変換を見てみる。
from datetime import datetime
import pytz
lasvegas = pytz.timezone('America/Los_Angeles') # アメリカ太平洋時間
for i in range(7):
dt = datetime.utcfromtimestamp(1572764400 + i * 1800)
# 1572764400はUTCで3日7時
print(dt.replace(tzinfo=utc).astimezone(lasvegas))
# =>
# 2019-11-03 00:00:00-07:00
# 2019-11-03 00:30:00-07:00
# 2019-11-03 01:00:00-07:00
# 2019-11-03 01:30:00-07:00
# 2019-11-03 01:00:00-08:00 <- 夏時間終了で時間が戻る
# 2019-11-03 01:30:00-08:00
# 2019-11-03 02:00:00-08:00
19分ずれる問題
pytz
から生成したタイムゾーンのオブジェクトでdt.replace(tzinfo=tokyo)
としたり、datetime(2019, 11, 3, 1, 0, 0, tzinfo=tokyo)
のようにすると、19分ずれる問題あり。日本時間だと19分ずれるが、アメリカ太平洋時間で試すと7分ずれる。
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import pytz
dt = datetime.utcfromtimestamp(1572742800)
print(dt)
# => 2019-11-03 01:00:00
# 1572764400は日本時間で3日1時
utc = timezone.utc
print(dt.replace(tzinfo=utc))
# => 2019-11-03 01:00:00+00:00
Naiveなdatetimeのインスタンスにreplace(tzinfo=utc)
とすると、+00:00
の情報を付与したAwareなインスタンスに変わる。utc
の代わりに日本時間を指定すると、
tokyo = pytz.timezone('Asia/Tokyo') # 日本時間
print(dt.replace(tzinfo=tokyo))
# => 2019-11-03 01:00:00+09:19
なぜか時差が19分ずれてしまう。
localize
というメソッドを使うと問題ない。なにが違うのかよくわからない。
print(tokyo.localize(dt))
# => 2019-11-03 01:00:00+09:00
アメリカ太平洋時間で試しても、ずれ幅は違うけどずれることに変わりない。
lasvegas = pytz.timezone('America/Los_Angeles') # アメリカ太平洋時間
print(dt.replace(tzinfo=lasvegas))
# => 2019-11-03 01:00:00-07:53
print(lasvegas.localize(dt))
# => 2019-11-03 01:00:00-08:00
夏時間が終わるタイミングでの挙動
アメリカ太平洋時間の3日は夏時間が終わる日で、上のほうの例で出てきたように、30分ごとの時間を羅列すると以下のように時間が流れる。
2019-11-03 00:00:00-07:00
2019-11-03 00:30:00-07:00
2019-11-03 01:00:00-07:00
2019-11-03 01:30:00-07:00
2019-11-03 01:00:00-08:00 <- 夏時間終了で時間が戻る
2019-11-03 01:30:00-08:00
2019-11-03 02:00:00-08:00
1時から1時59分までは2回めぐる。つまり、1時という情報だけでは1回目の1時なのか2回目の1時なのかはわからない曖昧性がある。
この事実を踏まえてlocalize
を試してみる。
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import pytz
dt = datetime.utcfromtimestamp(1572742800)
print(dt)
# => 2019-11-03 01:00:00
# dtは3日1時のNaiveなインスタンス
lasvegas = pytz.timezone('America/Los_Angeles') # アメリカ太平洋時間
print(lasvegas.localize(dt))
# => 2019-11-03 01:00:00-08:00
# 2回目の1時が返された
ちなみに2回目の1時の1時間前を計算すると0時になる。0時は本当は夏時間のはずだが冬時間のまま。
print(lasvegas.localize(dt) - timedelta(hours=1))
# => 2019-11-03 00:00:00-08:00
localize
する前のNaiveなインスタンスに対して1時間前を計算してからlocalize
すると、夏時間の0時になる。これは2回目の1時の2時間前。
print(lasvegas.localize(dt - timedelta(hours=1)))
# => 2019-11-03 00:00:00-07:00
以上。