Часовые пояса
Обзор
При включенном поддержке часовых поясов Django хранит информацию о датах и временах в формате UTC в базе данных, использует объекты datetime с учетом часового пояса во внутренней работе и преобразует их в часовой пояс конечного пользователя в шаблонах и формах.
Это удобно, если ваши пользователи проживают в нескольких часовых поясах, и вы хотите отображать информацию о датах и временах в соответствии с местным временем каждого пользователя.
Даже если ваш сайт доступен только в одном часовом поясе, всё равно рекомендуется хранить данные в UTC в вашей базе данных. Основная причина — летнее время (DST). Многие страны используют систему DST, где время переводится вперёд весной и назад осенью. Если вы работаете с местным временем, вы, вероятно, столкнетесь с ошибками дважды в год, когда происходят переходы. (Документация pytz подробно обсуждает эти проблемы.) Скорее всего, это не имеет значения для вашего блога, но это проблема, если вы переплачиваете или недоплачиваете своим клиентам на один час дважды в год, каждый год. Решение этой проблемы — использовать UTC в коде и использовать местное время только при взаимодействии с конечными пользователями.
Поддержка часовых поясов отключена по умолчанию. Чтобы включить её, установите USE_TZ =
True в вашем файле настроек. По умолчанию поддержка часовых поясов использует pytz, который устанавливается при установке Django; Django также поддерживает использование других реализаций часовых поясов, таких как zoneinfo, передавая объекты tzinfo напрямую функциям в django.utils.timezone.
Добавлена поддержка других реализаций часовых поясов, отличных от pytz.
Примечание
Файл настроек settings.py, созданный по умолчанию django-admin
startproject, включает USE_TZ = True для удобства.
Примечание
Также существует независимая, но связанная настройка USE_L10N, которая контролирует, будет ли Django активировать локализацию форматов. Смотрите Локализация форматов для получения дополнительной информации.
Если вы столкнулись с конкретной проблемой, начните с ЧАВО по часовым поясам.
Концепции
Обычные и явные объекты datetime
Объекты datetime.datetime Python обладают атрибутом tzinfo, который можно использовать для хранения информации о часовом поясе, представленной как экземпляр подкласса datetime.tzinfo. Когда этот атрибут установлен и описывает смещение, объект datetime является явным. В противном случае он обычный.
Вы можете использовать is_aware() и is_naive() для определения того, являются ли даты и время явными или обычными.
Когда поддержка часовых поясов отключена, Django использует обычные объекты datetime по местному времени. Это достаточно для многих случаев использования. В этом режиме для получения текущего времени вы напишете:
import datetime now = datetime.datetime.now()
Когда поддержка часовых поясов включена (USE_TZ=True), Django использует явные объекты datetime с учетом часового пояса. Если ваш код создаёт объекты datetime, они также должны быть явными. В этом режиме пример выше становится:
from django.utils import timezone now = timezone.now()
Предупреждение
Работа с явными объектами datetime не всегда интуитивна. Например, аргумент tzinfo стандартного конструктора datetime не работает надёжно для часовых поясов с DST. Использование UTC обычно безопасно; если вы используете другие часовые пояса, внимательно прочитайте документацию pytz.
Примечание
Объекты datetime.time Python также имеют атрибут tzinfo, а PostgreSQL имеет соответствующий тип time with time zone. Однако, как говорится в документации PostgreSQL, этот тип «обладает свойствами, которые приводят к сомнительной полезности».
Django поддерживает только обычные объекты времени и выведет исключение, если вы попытаетесь сохранить явный объект времени, так как часовой пояс для времени без связанной даты не имеет смысла.
Интерпретация обычных объектов datetime
Когда USE_TZ True, Django по-прежнему принимает обычные объекты datetime, чтобы сохранить обратную совместимость. Когда уровень базы данных получает такой объект, он пытается сделать его явным, интерпретируя его в по умолчанию часовой пояс и выводит предупреждение.
К сожалению, во время перехода на летнее время некоторые даты и время не существуют или являются неоднозначными. В таких ситуациях pytz выводит исключение. Поэтому вы всегда должны создавать явные объекты datetime с учетом часового пояса, когда поддержка часовых поясов включена.
На практике это редко представляет собой проблему. Django предоставляет вам явные объекты datetime в моделях и формах, и чаще всего новые объекты datetime создаются из существующих с помощью арифметики timedelta. Единственный объект datetime, который часто создается в коде приложения, — это текущее время, и timezone.now() автоматически выполняет правильные действия.
Часовой пояс по умолчанию и текущий часовой пояс
Часовой пояс по умолчанию — это часовой пояс, определенный настройкой TIME_ZONE.
Текущий часовой пояс — это часовой пояс, используемый для отображения.
Вы должны установить текущий часовой пояс на фактический часовой пояс конечного пользователя с помощью activate(). В противном случае используется часовой пояс по умолчанию.
Примечание
Как указано в документации TIME_ZONE, Django устанавливает переменные среды, чтобы процесс выполнялся в часовом поясе по умолчанию. Это происходит независимо от значения USE_TZ и текущего часового пояса.
Когда USE_TZ True, это полезно для сохранения обратной совместимости с приложениями, которые по-прежнему полагаются на местное время. Однако, как описано выше, это не полностью надёжно, и вы всегда должны работать с явными датами и временем в UTC в своём коде. Например, используйте fromtimestamp() и установите параметр tz в utc.
Выбор текущего часового пояса
Текущий часовой пояс эквивалентен текущему языковому стандарту для переводов. Однако нет эквивалента заголовку HTTP Accept-Language, который Django мог бы использовать для автоматического определения часового пояса пользователя. Вместо этого Django предоставляет функции выбора часовых поясов. Используйте их для создания логики выбора часового пояса, которая соответствует вашим потребностям.
Большинство веб-сайтов, которые заботятся о часовых поясах, спрашивают пользователей, в каком часовом поясе они живут, и хранят эту информацию в профиле пользователя. Для анонимных пользователей используется часовой пояс их основной аудитории или UTC. pytz предоставляет помощники, такие как список часовых поясов по стране, которые вы можете использовать для предварительного выбора наиболее вероятных вариантов.
Вот пример, который сохраняет текущий часовой пояс в сессии. (Он полностью пропускает обработку ошибок ради простоты.)
Добавьте следующий миддлвеар в MIDDLEWARE:
import pytz
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(pytz.timezone(tzname))
else:
timezone.deactivate()
return self.get_response(request)
Создайте представление, которое может установить текущий часовой пояс:
from django.shortcuts import redirect, render
def set_timezone(request):
if request.method == 'POST':
request.session['django_timezone'] = request.POST['timezone']
return redirect('/')
else:
return render(request, 'template.html', {'timezones': pytz.common_timezones})
Включите форму в template.html, которая будет POST в это представление:
{% 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 tz in timezones %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
<input type="submit" value="Set">
</form>
Формы с учетом часового пояса
При включении поддержки часовых поясов Django интерпретирует даты и время, введённые в формах, в текущем часовом поясе и возвращает явные объекты datetime в cleaned_data.
Если текущая временная зона вызывает исключение для дат и времён, которые не существуют или являются неоднозначными, потому что они попадают в переход DST (временные зоны, предоставляемые pytz, делают это), такие даты и времена будут отображаться как недопустимые значения.
Временные зоны в шаблонах
При включении поддержки временных зон Django преобразует объекты datetime с учётом временной зоны в текущую временную зону при их отображении в шаблонах. Это очень похоже на форматирование с учётом локализации.
Предупреждение
Django не преобразует объекты datetime без учёта временной зоны, потому что они могут быть неоднозначными и потому что ваш код никогда не должен генерировать объекты datetime без учёта временной зоны, когда включена поддержка временных зон. Однако вы можете принудительно выполнить преобразование с помощью фильтров шаблонов, описанных ниже.
Преобразование во временную зону пользователя не всегда уместно – вы можете генерировать выходные данные для компьютеров, а не для людей. Следующие фильтры и теги, предоставляемые библиотекой тегов шаблонов tz, позволяют управлять преобразованиями временных зон.
Теги шаблонов
localtime
Этот тег имеет точно такие же эффекты, как настройка USE_TZ с точки зрения движка шаблонов. Это позволяет более тонко контролировать преобразование.
Для активации или деактивации преобразования для блока шаблона используйте:
{% load tz %}
{% localtime on %}
{{ value }}
{% endlocaltime %}
{% localtime off %}
{{ value }}
{% endlocaltime %}
Примечание
Значение USE_TZ не учитывается внутри блока {% localtime %}.
timezone
Устанавливает или сбрасывает текущую временную зону в содержащемся блоке. Когда текущая временная зона сброшена, применяется значение по умолчанию.
{% load tz %}
{% timezone "Europe/Paris" %}
Paris time: {{ value }}
{% endtimezone %}
{% timezone None %}
Server time: {{ value }}
{% endtimezone %}
get_current_timezone
Вы можете получить имя текущей временной зоны, используя тег get_current_timezone:
{% get_current_timezone as TIME_ZONE %}
В качестве альтернативы, вы можете активировать обработчик контекста tz() и использовать переменную контекста TIME_ZONE.
Фильтры шаблонов
Эти фильтры принимают как объекты datetime с учётом временной зоны, так и без неё. Для преобразования они предполагают, что объекты datetime без учёта временной зоны находятся в часовой зоне по умолчанию. Они всегда возвращают объекты datetime с учётом временной зоны.
localtime
Принудительно преобразует одно значение в текущую временную зону.
Например:
{% load tz %}
{{ value|localtime }}
utc
Принудительно преобразует одно значение в UTC.
Например:
{% load tz %}
{{ value|utc }}
timezone
Принудительно преобразует одно значение в произвольную временную зону.
Аргумент должен быть экземпляром подкласса tzinfo или именем временной зоны.
Например:
{% load tz %}
{{ value|timezone:"Europe/Paris" }}
Руководство по миграции временных зон
Вот как мигрировать проект, который был запущен до того, как Django поддерживал временные зоны.
База данных
PostgreSQL
База данных PostgreSQL хранит даты и времена в формате timestamp with time zone. На практике это означает, что она преобразует даты и времена из временной зоны подключения в UTC при сохранении и из UTC во временную зону подключения при получении.
Следовательно, если вы используете PostgreSQL, вы можете свободно переключаться между USE_TZ
= False и USE_TZ = True. Временная зона подключения к базе данных будет установлена в TIME_ZONE или UTC соответственно, чтобы Django получал правильные даты и времена во всех случаях. Вам не нужно выполнять какие-либо преобразования данных.
Другие базы данных
Другие бэкэнды хранят даты и времена без информации о временной зоне. Если вы переключаетесь с USE_TZ = False на USE_TZ = True, вы должны преобразовать свои данные из местного времени в UTC – что не является детерминированным, если ваше местное время имеет DST.
Код
Первый шаг – добавить USE_TZ = True в файл настроек. На этом этапе всё должно работать в основном. Если вы создаёте объекты datetime без учёта временной зоны в своём коде, Django преобразует их с учётом временной зоны при необходимости.
Однако эти преобразования могут потерпеть неудачу при переходах между летним и зимним временем, что означает, что вы ещё не получаете все преимущества поддержки временных зон. Кроме того, вы, вероятно, столкнётесь с несколькими проблемами, потому что сравнивать объект datetime без учёта временной зоны с объектом datetime с учётом временной зоны невозможно. Поскольку Django теперь предоставляет вам объекты datetime с учётом временной зоны, вы получите исключения всякий раз, когда будете сравнивать datetime, полученный из модели или формы, с объектом datetime без учёта временной зоны, который вы создали в своём коде.
Поэтому второй шаг – переработать ваш код там, где вы инициализируете объекты datetime, чтобы сделать их с учётом временной зоны. Это можно сделать постепенно. django.utils.timezone определяет некоторые полезные вспомогательные функции для кода совместимости: now(), is_aware(), is_naive(), make_aware() и make_naive().
Наконец, чтобы помочь вам найти код, который нужно обновить, Django выводит предупреждение, когда вы пытаетесь сохранить объект 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',
)
Файлы данных
При сериализации datetime с учётом временной зоны смещение UTC включается, например:
"2011-09-01T13:20:30+03:00"
В то время как для datetime без учёта временной зоны – нет:
"2011-09-01T13:20:30"
Для моделей с полями DateTimeField это различие делает невозможным создание файла данных, который работает как с поддержкой временных зон, так и без неё.
Файлы данных, сгенерированные с помощью USE_TZ = False, или до Django 1.4, используют формат «без учёта временной зоны». Если ваш проект содержит такие файлы данных, после включения поддержки временных зон вы увидите предупреждения RuntimeWarning при их загрузке. Чтобы избавиться от предупреждений, вы должны преобразовать свои файлы данных в формат «с учётом временной зоны».
Вы можете перегенерировать файлы данных с помощью loaddata и затем dumpdata. Либо, если они достаточно малы, вы можете отредактировать их, чтобы добавить смещение UTC, соответствующее вашей настройке TIME_ZONE, к каждой сериализованной дате и времени.
FAQ по временным зонам
Настройка
-
Мне не нужны несколько временных зон. Нужно ли мне включить поддержку временных зон?
Да. Когда включена поддержка временных зон, Django использует более точную модель местного времени. Это защищает вас от тонких и не воспроизводимых ошибок при переходах на летнее и зимнее время (DST).
Когда вы включаете поддержку временных зон, вы столкнетесь с некоторыми ошибками, потому что используете даты и времена без учёта временной зоны там, где Django ожидает даты и времена с учётом временной зоны. Такие ошибки появляются при запуске тестов. Вы быстро научитесь, как избегать недопустимых операций.
С другой стороны, ошибки, вызванные отсутствием поддержки временных зон, гораздо сложнее предотвратить, диагностировать и исправить. Всё, что связано с запланированными задачами или арифметикой дат и времён, является кандидатом на тонкие ошибки, которые будут появляться только один или два раза в год.
По этим причинам поддержка временных зон включена по умолчанию в новых проектах, и вы должны её сохранить, если у вас нет очень веской причины для отказа от неё.
-
Я включил поддержку временных зон. Я в безопасности?
Возможно. Вы лучше защищены от ошибок, связанных с переходами на летнее и зимнее время, но вы всё ещё можете навредить себе, невнимательно преобразуя даты и времена без учёта временной зоны в даты и времена с учётом временной зоны и наоборот.
Если ваше приложение подключается к другим системам – например, если оно запрашивает веб-сервис – убедитесь, что даты и времена указаны должным образом. Для безопасной передачи дат и времён их представление должно включать смещение UTC или их значения должны быть в UTC (или и то, и другое!).
Наконец, наша система календаря содержит интересные граничные случаи. Например, вы не всегда можете напрямую вычесть один год из заданной даты:
>>> 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 минус один год 2011-02-28 или 2011-03-01, что зависит от ваших бизнес-требований.
-
Как взаимодействовать с базой данных, которая хранит даты и времена в местном времени?
Установите параметр
TIME_ZONEна соответствующую временную зону для этой базы данных в настройкеDATABASES.Это полезно для подключения к базе данных, которая не поддерживает временные зоны и не управляется Django, когда
USE_TZTrue.
Устранение неполадок
-
Моя программа аварийно завершается с
TypeError: can't compare offset-naiveand offset-aware datetimes– в чем проблема?Давайте воспроизведем эту ошибку, сравнив наивную и осознанную дату и время:
>>> import datetime >>> from django.utils import timezone >>> naive = datetime.datetime.utcnow() >>> aware = timezone.now() >>> naive == aware Traceback (most recent call last): ... TypeError: can't compare offset-naive and offset-aware datetimes
Если вы столкнулись с этой ошибкой, скорее всего, ваш код сравнивает эти две вещи:
- дата и время, предоставленная Django – например, значение, считанное из формы или поля модели. Поскольку вы включили поддержку часовых поясов, она осознанная.
- дата и время, сгенерированная вашим кодом, которая является наивной (иначе вы бы этого не читали).
В общем случае правильное решение – изменить ваш код, чтобы использовать осознанную дату и время.
Если вы пишете подключаемый модуль, который должен работать независимо от значения
USE_TZ, вы можете найтиdjango.utils.timezone.now()полезным. Эта функция возвращает текущую дату и время как наивную дату и время, когдаUSE_TZ = Falseи как осознанную дату и время, когдаUSE_TZ = True. Вы можете добавлять или вычитатьdatetime.timedeltaпо мере необходимости. -
Я вижу много
RuntimeWarning: DateTimeField received a naive datetime(YYYY-MM-DD HH:MM:SS)while time zone support is active– это плохо?Когда поддержка часовых поясов включена, уровень базы данных ожидает получения только осознанных дат и времени от вашего кода. Это предупреждение появляется, когда он получает наивную дату и время. Это означает, что вы не завершили перенос своего кода для поддержки часовых поясов. Обратитесь к руководству по миграции руководству по миграции за советами по этому процессу.
Тем временем, для обратной совместимости, дата и время считаются по умолчанию в часовом поясе, который, как правило, и ожидается.
-
now.date()вчера! (или завтра)Если вы всегда использовали наивные даты и время, вы, вероятно, считаете, что можете преобразовать дату и время в дату, вызвав его метод
date(). Вы также считаете, чтоdateочень похожа наdatetime, за исключением того, что она менее точна.Ничего из этого неверно в среде с поддержкой часовых поясов:
>>> import datetime >>> import pytz >>> paris_tz = pytz.timezone("Europe/Paris") >>> new_york_tz = pytz.timezone("America/New_York") >>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30)) # This is the correct way to convert between time zones with pytz. >>> new_york = new_york_tz.normalize(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=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>) >>> new_york datetime.datetime(2012, 3, 2, 19, 30, tzinfo=<DstTzInfo 'America/New_York' EST-1 day, 19:00:00 STD>)Как показывает этот пример, одна и та же дата и время имеют разные даты в зависимости от часового пояса, в котором она представлена. Но настоящая проблема фундаментальнее.
Дата и время представляют собой точку во времени. Она абсолютна: она ни от чего не зависит. Напротив, дата — это понятия календаря. Это период времени, границы которого зависят от часового пояса, в котором рассматривается дата. Как вы видите, эти два понятия принципиально отличаются, и преобразование даты и времени в дату — это не детерминированная операция.
Что это означает на практике?
В общем случае следует избегать преобразования
datetimeвdate. Например, вы можете использовать фильтр шаблоновdate, чтобы отобразить только часть даты даты и времени. Этот фильтр преобразует дату и время в текущий часовой пояс перед форматированием, гарантируя, что результаты будут отображаться правильно.Если вам действительно нужно выполнить это преобразование самостоятельно, вы должны убедиться, что сначала дата и время преобразованы в соответствующий часовой пояс. Обычно это будет текущий часовой пояс:
>>> from django.utils import timezone >>> timezone.activate(pytz.timezone("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() # Again, this is the correct way to convert between time zones with pytz. >>> local = current_tz.normalize(paris.astimezone(current_tz)) >>> local datetime.datetime(2012, 3, 3, 8, 30, tzinfo=<DstTzInfo 'Asia/Singapore' SGT+8:00:00 STD>) >>> 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". Как я могу преобразовать ее в осознанную дату и время?Для этого как раз и предназначен pytz.
>>> from django.utils.dateparse import parse_datetime >>> naive = parse_datetime("2012-02-21 10:28:45") >>> import pytz >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None) datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=<DstTzInfo 'Europe/Helsinki' EET+2:00:00 STD>)Обратите внимание, что
localize— это расширение pytz для APItzinfo. Также вы можете захотеть обработатьpytz.InvalidTimeError. Документация pytz содержит больше примеров. Вы должны ознакомиться с ней перед попыткой манипулирования осознанными датами и временем. -
Как я могу получить местное время в текущем часовом поясе?
Ну, первый вопрос – вам это действительно нужно?
Вы должны использовать местное время только при взаимодействии с людьми, а слой шаблонов предоставляет фильтры и теги для преобразования дат и времени в часовой пояс по вашему выбору.
Кроме того, Python умеет сравнивать осознанные даты и время, учитывая смещения UTC при необходимости. Гораздо проще (и возможно быстрее) писать весь код моделей и представлений в UTC. Поэтому в большинстве случаев даты и время в UTC, возвращаемые
django.utils.timezone.now(), будет достаточно.Однако, если вы действительно хотите получить местное время в текущем часовом поясе, вот как это можно сделать:
>>> from django.utils import timezone >>> timezone.localtime(timezone.now()) datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
В этом примере текущий часовой пояс –
"Europe/Paris". -
Как я могу увидеть все доступные часовые пояса?
pytz предоставляет помощники, включая список текущих часовых поясов и список всех доступных часовых поясов – некоторые из которых представляют лишь исторический интерес.
zoneinfoтакже предоставляет аналогичную функциональность черезzoneinfo.available_timezones().
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/3.2/topics/i18n/timezones/