タイムゾーン¶
概要¶
タイムゾーンのサポートが有効のとき、Django は内部的にタイムゾーンを意識した (aware な) タイムゾーンオブジェクトを使用して、データベース内に UTC で日時の情報を保持します。そして、テンプレートやフォーム内でエンドユーザのタイムゾーンに変換します。
この機能は、ユーザが複数のタイムゾーンを使用しており、またユーザに対して彼らの時計と同じ日時を表示したいときに有用です。
あなたのウェブサイトが1つのタイムゾーンでしか利用できない場合でも、データベースにUTCでデータを保存するのはよいことです。主な理由はサマータイム(DST)です。多くの国では、春に時計を進め、秋に時計を戻すサマータイム制度を採用しています。現地時間で作業している場合、年に2回、この移行時にエラーが発生する可能性があります。あなたのブログには関係ないことかもしれませんが、毎年2回、顧客に1時間ずつ過大請求したり過小請求したりするのは問題です。この問題の解決策は、コード内でUTCを使用し、エンドユーザーとのやりとりの時だけローカルタイムを使用することです。
タイムゾーンのサポートはデフォルトで有効になっています。無効にするには、設定ファイルで USE_TZ = False
を設定してください。
古いバージョンでは、タイムゾーンのサポートはデフォルトで無効になっていました。
タイムゾーンのサポートには zoneinfo
を使います。これは Python 3.9 から Python 標準ライブラリの一部となりました。
特定の問題と格闘している場合は、タイムゾーンの FAQ を参照してください。
コンセプト¶
native と aware の日時オブジェクト¶
Python の datetime.datetime
のオブジェクトには、タイムゾーン情報を保持するために使える tzinfo
属性があり、これは datetime.tzinfo
のサブクラスのインスタンスで表されます。 この属性がセットされオフセットを示すとき、datetime オブジェクトは aware (タイムゾーンを意識した日時) となります。それ以外の場合は naive (タイムゾーンを意識しない素朴な日時) となります。
is_aware()
is_naive()
を使って、datatime を aware にするか native にするかを決めることもできます。
タイムゾーンのサポートが無効になっている場合、 Django はローカル時間の naive な datetime オブジェクトを使います。多くの場合、これで十分です。このモードでは、現在時刻を取得するには、次のように書きます:
import datetime
now = datetime.datetime.now()
タイムゾーンサポートが有効化 (USE_TZ=True
) されているときは、Django は タイムゾーンを認識する (awareな) datetime オブジェクトを使用します。コード内で datetime オブジェクトを作成した場合も、aware となります。このモードでは、上の例は以下のようになります:
from django.utils import timezone
now = timezone.now()
警告
認識された datetime オブジェクトを扱うことは、必ずしも直感的ではありません。例えば、標準の datetime コンストラクタの tzinfo
引数は、夏時間を含むタイムゾーンでは正しく動作しません。他のタイムゾーンを使用する場合は、 zoneinfo
のドキュメントをよく確認してください。
注釈
Python の datetime.time
オブジェクトは、tzinfo
属性を持ちます。また PostgreSQL には、これに相当する time with time zone
タイプがあります。ただし、PostgreSQL のドキュメントにあるとおり、このタイプは "不確かな有用性に導く特性を示しています"。
Django は naive な time オブジェクトのみをサポートしており、aware な time オブジェクトを保存しようとすると例外を投げます。これは、日付を伴わない時刻に対するタイムゾーンには意味がないからです。
native な datetime オブジェクトの変換¶
後方互換性を維持するため、 USE_TZ
が True
のときでも、Django は naive な datetime オブジェクトを受け入れます。データベースレイヤがそのようなオブジェクトを受け取ると、デフォルトのタイムゾーン として解釈して aware に変換しようと試み、警告を発します。
残念ながら、夏時間(DST)の移行中には、存在しない日時や曖昧な日時があります。そのため、タイムゾーンサポートが有効な場合は常に aware な日時オブジェクトを作成するべきです。(DST移行中に日時に適用すべきオフセットを指定するために fold
属性を使用する例については、zoneinfo ドキュメントの Using ZoneInfo セクション
を参照してください。)
実際には、これはめったに問題になりません。Django はモデルやーフォームでは aware な datetime オブジェクトを渡し、ほとんどの場合、新しい datetime オブジェクトは timedelta
計算を通じて他の日時から作成されます。唯一アプリケーションのコード内でよく作成される datetime は現在の時刻で、timezone.now()
は自動的に適切な処理を行います。
デフォルトタイムゾーンとカレントタイムゾーン¶
デフォルトタイムゾーン は、TIME_ZONE
設定によって定義されるタイムゾーンです。
カレントタイムゾーン は、レンダリングに使われるタイムゾーンです。
activate()
を使って、エンドユーザの実際のタイムゾーンに対するカレントタイムゾーンをセットする必要があります。 そうしないと、デフォルトタイムゾーンが使用されます。
カレントタイムゾーンを選択する¶
カレントタイムゾーンは、翻訳に対する locale に相当します。ただし、Django がユーザのタイムゾーンを自動的に決めるために使える Accept-Language
HTTP ヘッダに相当するものはありません。代わりに、Django は タイムゾーン選択関数 を提供しています。これらを使って、あなたにとって有用なタイムゾーン選択ロジックを組み立ててください。
タイムゾーンを気にするほとんどのウェブサイトは、ユーザーにどのタイムゾーンに住んでいるかを尋ね、この情報をユーザーのプロフィールに保存します。匿名ユーザーの場合は、主な視聴者のタイムゾーンまたはUTCを使用します。 zoneinfo.available_timezones()
は利用可能なタイムゾーンのセットを提供し、ロケーションからタイムゾーンへのマッピングを構築できます 。
以下は、セッション内にカレントタイムゾーンを保持する例です。(シンプルにするため、全体的にエラー処理を省略しています。)
以下のミドルウェアを MIDDLEWARE
に追加します:
import zoneinfo
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = request.session.get("django_timezone")
if tzname:
timezone.activate(zoneinfo.ZoneInfo(tzname))
else:
timezone.deactivate()
return self.get_response(request)
カレントタイムゾーンをセットできるビューを作成します:
from django.shortcuts import redirect, render
# Prepare a map of common locations to timezone choices you wish to offer.
common_timezones = {
"London": "Europe/London",
"Paris": "Europe/Paris",
"New York": "America/New_York",
}
def set_timezone(request):
if request.method == "POST":
request.session["django_timezone"] = request.POST["timezone"]
return redirect("/")
else:
return render(request, "template.html", {"timezones": common_timezones})
このビューに POST
するフォームを template.html
に入れます:
{% load tz %}
{% get_current_timezone as TIME_ZONE %}
<form action="{% url 'set_timezone' %}" method="POST">
{% csrf_token %}
<label for="timezone">Time zone:</label>
<select name="timezone">
{% for city, tz in timezones %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
{% endfor %}
</select>
<input type="submit" value="Set">
</form>
フォームでのタイムゾーン aware な入力¶
タイムゾーンサポートを有効にしたとき、Django はフォーム内に入力された日時を カレントタイムゾーン で変換し、cleaned_data
内で aware な datetime オブジェクトを返します。
変換されたdatetimeが存在しないか、または夏時間の移行期間中であるためにあいまいな場合は、無効な値として報告されます。
テンプレートでのタイムゾーン aware な出力¶
タイムゾーンサポートを有効にしているとき、Django はテンプレートでレンダリングする際に aware な datetime オブジェクトを カレントタイムゾーン に変換します。この動作は、 表示形式のローカライズ と非常によく似ています。
警告
Django は naive な日時オブジェクトは変換しません。これは、曖昧であることと、タイムゾーンサポートが有効化されているときは naive な日時を生成すべきではないからです。ただし、後述するテンプレートフィルタを使えば強制的に変換させることもできます。
現地時刻への転換は、適切ではないことがあります -- 人手はなくコンピュータに対してアウトプットを生成するときなどです。以下のフィルタやタグ (tz
テンプレートタグライブラリによって提供されています) で、タイムゾーンの転換をコントロールできます。
テンプレートフィルタ¶
以下のフィルタは、aware と native の日時の両方を受け入れます。転換するために、これらは native な日時はデフォルトタイムゾーンにあると仮定します。これらは常に aware な日時を返します。
localtime
¶
単一の値からカレントタイムゾーンへの変換を強制します。
例えば次のようにします:
{% load tz %}
{{ value|localtime }}
utc
¶
単一の値から UTC への転換を強制します。
例えば次のようにします:
{% load tz %}
{{ value|utc }}
timezone
¶
単一の値から指定したタイムゾーンへの転換を強制します。
引数は tzinfo
サブクラスのインスタンスか、タイムゾーンの名前である必要があります。
例えば次のようにします:
{% load tz %}
{{ value|timezone:"Europe/Paris" }}
マイグレーションガイド¶
以下は、Django がタイムゾーンをサポートする以前に開始したプロジェクトをマイグレーションする方法です。
データベース¶
PostgreSQL¶
PostgreSQL バックエンドは datetime を timestamp with time zone
として保存します。実際には、これが意味するのは、datetime をコネクションのタイムゾーンからストレージ上では UTC に変換して、取得時には UTC からコネクションのタイムゾーンに変換するということです。
結果として、PostgreSQL を使っている場合、 USE_TZ = False
と USE_TZ = True
を自由に切り替えることができます。データベース接続のタイムゾーンはそれぞれ DATABASE-TIME_ZONE
か UTC
に設定されるので、Django はどんな場合でも正しい日時を取得できます。データ変換を行う必要はありません。
他のデータベース¶
他のバックエンドは、タイムゾーン情報なしで日時を保持します。USE_TZ = False
から USE_TZ = True
に切り替えた場合、現地時刻から UTC に転換する必要があります -- あなたの現地時刻に DST がある場合、これは確実に 1 つに決まるわけではありません。
コード¶
最初のステップは、設定ファイルに USE_TZ = True
を追加することです。この時点では、だいたいのことがうまく動くはずです。コード内で naive な datetime オブジェクトを生成した場合、Django は必要に応じて aware にします。
ただし、これらの変換ではDSTの移行が失敗する可能性があります。これは、まだ対ー無ゾーンサポートの利点が完全には得られていないことを意味します。 また、naive な日時と aware な日時を比較することは不可能なため、いくつかの問題に遭遇する可能性があります。 Django はaware な日時を提供するので、モデルやフォームから来た datetime と、コード内に作成した naive な datetime を比較すると例外が発生します。
したがって、第2のステップは、datetime オブジェクトをインスタンス化している部分を aware に変更するため、コードをリファクタリングすることです。 これは段階的に行うことができます。django.utils.timezone
は、互換性のための便利なヘルパーをいくつ定義しています now()
、is_aware()
、is_naive()
、make_aware()
そして make_naive()
です。
最後に、アップグレードが必要なコードを見つけやすくするために、Django は naive な datetime をデータベースに保存しようとすると警告を出します:
RuntimeWarning: DateTimeField ModelName.field_name received a naive
datetime (2012-01-01 00:00:00) while time zone support is active.
開発中、以下を設定ファイルに追加することで、これらの警告を例外として発生するように変更し、トラックバックを得ることができます:
import warnings
warnings.filterwarnings(
"error",
r"DateTimeField .* received a naive datetime",
RuntimeWarning,
r"django\.db\.models\.fields",
)
フィクスチャ (fixture)¶
aware な日時をシリアライズするとき、以下のように UTC オフセットが含まれます:
"2011-09-01T13:20:30+03:00"
しかし、naive な日付の場合はそうではありません:
"2011-09-01T13:20:30"
DateTimeField
を使ったモデルに対しては、この違いにより、タイムゾーンサポートのありとなし両方に対して動作するフィクスチャを記述することができません。
USE_TZ = False
もしくは Django 1.4 以前 で生成されたフィクスチャは、"naive" 形式を使用します。このようなフィクスチャがプロジェクトに含まれる場合、タイムゾーンサポートを有効化した後に、これらをロードする際に RuntimeWarning
を参照してください。警告を破棄するには、これらのフィクスチャを "aware" 形式に転換する必要があります。
loaddata
と dumpdata
でフィクスチャを再生成できます。または、それらが十分に小さい場合、シリアライズされた各日時に TIME_ZONE
に一致する UTC オフセットを追加するように編集できます。
FAQ¶
セットアップ¶
複数のタイムゾーンを必要としません。タイムゾーンサポートを有効化するべきですか?
はい。タイムゾーンのサポートを有効にすると、 Django はより正確な現地時間のモデルを使います。これにより、夏時間 (DST) の遷移にまつわる微妙で再現不可能なバグを回避できます。
タイムゾーンのサポートを有効にすると、Django の aware な datetime を期待しているところで naive な datetime を使っていることが原因のエラーが発生します。このようなエラーはテストを実行するときに現れます。無効な操作を避ける方法をすぐに知ることができるでしょう。
一方、タイムゾーンのサポートがないために起こるバグは、予防、診断、修正が非常に困難です。スケジュールされたタスクや datetime 演算を含むものは、年に1度か2度しか発生しない微妙なバグの原因になります。
このような理由から、新しいプロジェクトではタイムゾーンのサポートがデフォルトで有効になっています。
タイムゾーンサポートを有効化しました。私は安全ですか?
おそらく。DST 関連のバグからは守られるようになりましたが、それでも不用意な変換 ( naive な日時を aware な日時に変更する、およびその逆)ことによって自ら墓穴を掘る可能性があります。
アプリケーションが他のシステムに接続する場合、例えばウェブサービスにクエリする場合、datetime が適切に指定されていることを確認してください。datetime を安全に送信するには、その表現に UTC オフセットを含めるか、その値を UTC にする必要があります(あるいはその両方!)。
最後に、私たちのカレンダーシステムには興味深いエッジケースがあります。例えば、ある日付から直接1年を引くことはできません:
>>> import datetime >>> def one_year_before(value): # Wrong example. ... return value.replace(year=value.year - 1) ... >>> one_year_before(datetime.datetime(2012, 3, 1, 10, 0)) datetime.datetime(2011, 3, 1, 10, 0) >>> one_year_before(datetime.datetime(2012, 2, 29, 10, 0)) Traceback (most recent call last): ... ValueError: day is out of range for month
このような関数を正しく実装するには、2012-02-29から1年を引いた値が2011-02-28なのか、2011-03-01なのかを決める必要があります。
現地時刻で日時を保持しているデータベースと、どのようにやり取りすればいいですか?
TIME_ZONE
オプションを、DATABASES
設定内のデータベースに適したタイムゾーンにセットしてください。これは、
USE_TZ
がTrue
のときに、タイムゾーンをサポートしておらず、 Django が管理していないデータベースに接続するのに便利です。
トラブルシューティング¶
私のアプリケーションが
TypeError: can't compare offset-naive
and offset-aware datetimes
とともにクラッシュします -- 何がいけませんか?naive な datetime と aware な datetime を比較して、このエラーを再現してみましょう:
>>> from django.utils import timezone >>> aware = timezone.now() >>> naive = timezone.make_naive(aware) >>> naive == aware Traceback (most recent call last): ... TypeError: can't compare offset-naive and offset-aware datetimes
このエラーに遭遇した場合、あなたのコードはこれら 2 つを比較している可能性が高いです:
Django が提供する datetime -- 例えば、フォームやモデルフィールド から読み込んだ値。タイムゾーンのサポートが有効になっているので、aware です。
あなたのコードによって生成された datetime。これは naive です(そうでなければ、あなたはこれを読んでいないでしょう)。
通常、正しい解決策は、代わりに aware な datetime を使うようにコードを変更することです。
USE_TZ
の値とは関係なく動作するようなプラグイン可能なアプリケーションを書いている場合、django.utils.timezone.now()
が役に立つかもしれません。この関数は、USE_TZ = False
の場合は naive な datetime として、USE_TZ = True
の場合は aware な datetime として現在の日時を返します。必要に応じてdatetime.timedelta
を足したり引いたりすることができます。多くの
RuntimeWarning: DateTimeField received a naive datetime
(YYYY-MM-DD HH:MM:SS)
while time zone support is active
が発生します。 -- これは悪いことですか?タイムゾーンのサポートが有効になっている場合、データベース層はコードから aware な datetime だけを受け取ることを期待します。この警告は、naive な datetime を受け取ったときに発生します。これは、タイムゾーンをサポートするためのコードの移植が完了していないことを示しています。このプロセスのヒントについては マイグレーションガイド を参照してください。
一方、後方互換性のために、datetime はデフォルトのタイムゾーンにあるとみなされます。
now.date()
が昨日 (もしくは明日) になります!常に naive な日時を使用してきた場合、
date()
メソッドを呼び出すことで日時を日付に変換できると考えているかもしれません。また、date
はdatetime
に非常に似ているが、精度が低いとも考えるでしょう。タイムゾーンを意識した環境では、どれも正しくありません:
>>> import datetime >>> import zoneinfo >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris") >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York") >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz) # This is the correct way to convert between time zones. >>> new_york = paris.astimezone(new_york_tz) >>> paris == new_york, paris.date() == new_york.date() (True, False) >>> paris - new_york, paris.date() - new_york.date() (datetime.timedelta(0), datetime.timedelta(1)) >>> paris datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris')) >>> new_york datetime.datetime(2012, 3, 2, 19, 30, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))
この例が示すように、同じdatetimeでも、それを表現するタイムゾーンによって日付が異なります。しかし、本当の問題はもっと根本的なところにあります。
datetime は 時点 を表します。それは絶対的なもので、何にも依存しません。逆に、日付は カレンダーの概念 です。その境界は日付が扱われるタイムゾーンに依存します。このように、この2つの概念は根本的に異なっており、datetime を date に変換することは決定論的な操作ではありません。
これは実際のところ何を意味するのでしょうか?
一般的に、
datetime
をdate
に変換することは避けるべきです。例えば、date
テンプレートフィルタを使うと、datetime の日付部分だけを表示できます。このフィルタは datetime をフォーマットする前にカレントタイムゾーンに変換し、結果が正しく表示されるようにします。どうしても自分で変換する必要がある場合は、まず datetime が適切なタイムゾーンに変換されていることを確認する必要があります。通常、これはカレントタイムゾーンになります:
>>> from django.utils import timezone >>> timezone.activate(zoneinfo.ZoneInfo("Asia/Singapore")) # For this example, we set the time zone to Singapore, but here's how # you would obtain the current time zone in the general case. >>> current_tz = timezone.get_current_timezone() >>> local = paris.astimezone(current_tz) >>> local datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore')) >>> local.date() datetime.date(2012, 3, 3)
エラーが発生しました "
Are time zone definitions for your database installed?
"MySQLを使用している場合は、MySQLノートの タイムゾーンの定義 セクションを参照してタイムゾーン定義を読み込んでください。
使い方¶
文字列
"2012-02-21 10:28:45"
があり、"Europe/Helsinki"
タイムゾーンであることが分かっています。aware な日時に変換するにはどうすればよいですか?この場合、必要な
ZoneInfo
インスタンスを作成し、それを naive な datetime に付加する必要があります。>>> import zoneinfo >>> from django.utils.dateparse import parse_datetime >>> naive = parse_datetime("2012-02-21 10:28:45") >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki")) datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki'))
カレントタイムゾーンで現地時刻を取得するにはどうすればよいですか?
そうですね。最初に質問させてください。それは本当に必要ですか?
人間とやり取りするときだけ、現地時刻を使うべきです。そして、テンプレートレイヤは日時をあなたが選択したタイムゾーンに転換するための フィルタとタグ を提供しています。
さらに、Python は認識された datetime を比較する方法を知っており、必要に応じて UTC オフセットを考慮します。すべてのモデルやビューのコードを UTC で書く方がずっと簡単です (そしておそらく速い)。ですから、ほとんどの場合、
django.utils.timezone.now()
で返される UTC での datetime で十分です。ただし、完全のため、カレントタイムゾーンの現地時間が本当に必要な場合は、以下の方法で取得できます:
>>> from django.utils import timezone >>> timezone.localtime(timezone.now()) datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
この例では、カレントタイムゾーンは
"Europe/Paris"
です。全ての利用可能なタイムゾーンを参照するにはどうすればいいですか?
zoneinfo.available_timezones()
は、あなたのシステムで利用可能なIANAタイムゾーンの全ての有効なキーを提供します。使用上の注意についてはドキュメントを参照してください。