LoginSignup
10
4

More than 3 years have passed since last update.

Pythonでのタイムゾーンの扱い(datetime, pytz)

Posted at

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

以上。

10
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4