Часовые пояса¶
Обзор¶
Когда поддержка часовых поясов включена, Django сохраняет информацию о дате и времени в формате UTC в базе данных, использует внутренние объекты datetime с учетом часового пояса и преобразует их в часовой пояс конечного пользователя в формах. В шаблонах будет использоваться часовой пояс по умолчанию, но его можно обновить до часового пояса конечного пользователя с помощью фильтров и тегов.
Такая поддержка становится актуальной, если ваши пользователи живут в разных часовых поясах и требуется отображать время в их текущем часовом поясе.
Даже если ваш веб-сайт доступен только в одном часовом поясе, хранить данные в базе данных в формате UTC все равно рекомендуется. Основная причина – переход на летнее время (DST). Во многих странах существует система летнего времени, при которой часы переводят вперед весной и назад осенью. Если вы работаете по местному времени, вы, скорее всего, столкнетесь с ошибками дважды в год, когда происходят переходы. Это, вероятно, не имеет значения для вашего блога, но это проблема, если вы завышаете или занижаете счет своим клиентам на один час два раза в год, каждый год. Решение этой проблемы — использовать в коде UTC и использовать местное время только при взаимодействии с конечными пользователями.
Поддержка часовых поясов включена по умолчанию. Чтобы отключить его, установите USE_TZ = False в файле настроек.
Для поддержки часовых поясов используется zoneinfo, который является частью стандартной библиотеки Python из Python 3.9.
Если вы бьётесь над определённой проблемой, начните с ЧаВо по часовым поясам.
Концепция¶
Объекты относительного и абсолютного времени¶
Объекты класса datetime.datetime обладают атрибутом tzinfo, который может быть использован для хранения информации о часовом поясе и представлен потомком класса datetime.tzinfo. Если этот атрибут установлен и описывает смещение, то объект описывает абсолютное время. Иначе речь идёт об относительном времени.
Вы можете использовать функции is_aware() и is_naive() для определения типа объекта.
При отключенной поддержке часовых поясов, Django использует относительное время. Такое поведение подходит для многих случаев. В этом режиме, для получения информации о текущем времени, потребуется сделать:
import datetime
now = datetime.datetime.now()
При включенной поддержке часовых поясов (USE_TZ=True), Django использует время с указанным часовым поясом. Если ваш код создаёт объекты даты-времени, они тоже должны быть представлены с указанным часовым поясом. В этом режиме, предыдущий пример будет выглядеть так:
from django.utils import timezone
now = timezone.now()
Предупреждение
Работа с объектами datetime не всегда интуитивно понятна. Например, аргумент tzinfo стандартного конструктора datetime не работает надежно для часовых поясов с летним временем. Использование UTC в целом безопасно; если вы используете другие часовые пояса, вам следует внимательно просмотреть документацию zoneinfo.
Примечание
Объекты класса datetime.time также обладают атрибутом tzinfo, а PostgreSQL имеет соответствующий тип time with time zone. Тем не менее, как написано в документации на PostgreSQL, этот тип «обладает свойствами, которые имеют сомнительную полезность».
Django поддерживает только объекты времени без часового пояса и вызывает исключение, если вы попытаетесь сохранить объект с часовым поясом, так как часовой пояс без даты не имеет смысла.
Интерпретация объектов относительного времени¶
Если параметр конфигурации USE_TZ установлен в True, Django продолжает принимать объекты относительного времени, чтобы обеспечить обратную совместимость. Когда слой базы данных получает такой объект, он пытается преобразовать его в объект абсолютного времени, интерпретируя его в часовом поясе по умолчанию и выбрасывает предупреждение.
К сожалению, во время перехода на летнее время некоторые даты и время не существуют или являются неоднозначными. Вот почему вы всегда должны создавать осведомленные объекты datetime, когда включена поддержка часовых поясов. (См. раздел Using ZoneInfo документации Zoneinfo, где приведены примеры использования атрибута fold для указания смещения, которое должно применяться к дате и времени во время перехода на летнее время.)
На практике это редко является проблемой. Django предоставляет вам объекты абсолютного времени в моделях и формах и чаще всего, новые объекты даты создаются из уже имеющихся с помощью timedelta арифметики. Чаще всего в коде используется текущая дата и функция timezone.now () создаёт объект правильного типа.
Стандартный часовой пояс и текущий часовой пояс¶
Стандартный часовой пояс — это часовой пояс, определённый параметром конфигурации TIME_ZONE.
Текущая часовой пояс — часовой пояс для которого отображается страница сайта.
Вы должны настраивать текущий часовой пояс на пояс пользователя с помощью activate(). В противном случае будет использоваться стандартный часовой пояс.
Примечание
Как указано в документации на параметр TIME_ZONE, Django устанавливает переменные окружения так, что она работает в стандартном часовом поясе. Это происходит вне зависимости от значения переменой USE_TZ и текущего часового пояса.
Когда USE_TZ имеет значение True, это полезно для сохранения обратной совместимости с приложениями, которые по-прежнему полагаются на местное время. Однако, как объяснялось выше,, это не совсем надежно, и вам всегда следует работать с учетом даты и времени в формате UTC в своем собственном коде. Например, используйте fromtimestamp() и установите для параметра tz значение datetime.UTC.
Выбор текущего часового пояса¶
Текущий часовой пояс соответствует текущей локали для переводов. Тем не менее, не существует аналога для HTTP заголовка Accept-Language, который Django может использовать для автоматического определения часового пояса пользователя. Вместо этого, Django предоставляет функции выбора часового пояса. Используйте их при построении логики выбора часового пояса.
Большинство веб-сайтов, которым важны часовые пояса, спрашивают пользователей, в каком часовом поясе они живут, и сохраняют эту информацию в профиле пользователя. Для анонимных пользователей используется часовой пояс их основной аудитории или UTC. zoneinfo.available_timezones() предоставляет набор доступных часовых поясов, которые вы можете использовать для построения карты вероятных мест и часовых поясов.
Ниже приведён пример сохранения текущего часового пояса в сессии. (Пример не использует обработку ошибок для простоты.)
Добавьте следующую строку в MIDDLEWARE_CLASSES:
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})
Добавьте форму в 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 city, tz in timezones.items %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
{% endfor %}
</select>
<input type="submit" value="Set">
</form>
Ввод значений абсолютного времени в формы¶
После активации поддержки часовых поясов, Django интерпретирует значения времени, вводимые в формы относительно текущего часового пояса, и возвращает объекты абсолютного времени в cleaned_data.
Преобразованные даты и время, которые не существуют или являются неоднозначными, поскольку они попадают в переход на летнее время, будут отображаться как недопустимые значения.
Вывод объектов абсолютного времени в шаблонах¶
После активации поддержки временных зон, Django преобразует объекты абсолютного времени в текущий часовой пояс при отображении в шаблоне. Процесс идёт аналогично локализации форматирования.
Предупреждение
Django не преобразует объекты относительного времени, так как они могут иметь двусмысленное значение. Ваш код не должен создавать объекты относительного времени при включенной поддержке часовых поясов. Тем не менее, вы можете включить принудительную конвертацию с помощью шаблонных фильтров, описанных далее.
Преобразование к локальному времени не всегда полезно, так как данные могут генерироваться для компьютеров, а не для людей. Следующие фильтры и теги, предоставляемые шаблонной библиотекой тегов tz, позволяют вам управлять преобразованиями объектов времени.
Шаблонные фильтры¶
Эти фильтры работают с обоими типами объектов времени. Для обеспечения конвертации, они предполагают, что значения относительного времени представлены в текущем часовом поясе. Эти фильтры всегда возвращают значения абсолютного времени.
localtime¶
Обеспечивает конвертацию единственного значения в текущий часовой пояс.
Например:
{% load tz %}
{{ value|localtime }}
utc¶
Преобразовывает единственное значение к UTC.
Например:
{% load tz %}
{{ value|utc }}
timezone¶
Преобразовывает единственное значение к указанному часовому поясу.
Аргумент должен быть экземпляром потомка класса tzinfo или именем часового пояса. В последнем случае необходимо наличие pytz_.
Например:
{% load tz %}
{{ value|timezone:"Europe/Paris" }}
Руководство по миграции¶
Здесь описано как провести миграцию старого проекта для поддержки часовых поясов.
База данных¶
PostgreSQL¶
Бэкенд PostgreSQL сохраняет значения времени как timestamp with time zone. Это означает, что он преобразовывает значение в UTC при сохранении и обратно при чтении.
Как следствие, если вы используете PostgreSQL, вы можете свободно переключаться между USE_TZ = False и USE_TZ = True. Часовой пояс подключения к базе данных будет установлен на DATABASE-TIME_ZONE или UTC соответственно, чтобы Django во всех случаях получал правильные даты и время. Вам не нужно выполнять какие-либо преобразования данных.
Настройки часового пояса
часовой пояс, настроенный для соединения в настройке DATABASES, отличается от общей настройки TIME_ZONE.
Другие базы данных¶
Остальные бэкенды сохраняют значения времени без информации о часовом поясе. Если вы переключите с USE_TZ = False на USE_TZ = True, вам придётся преобразовать значения из локального времени в UTC, что не слишком радует, если у вас используется летнее время.
Код¶
Первым шагом надо добавить USE_TZ = True в конфигурационный файл и установить pytz_ (по возможности). После этого почти всё должно заработать. Если вы создаёте объекты относительного времени в вашем коде, Django будет преобразовывать их в объекты абсолютного времени при необходимости.
Тем не менее, эти преобразования могут ошибаться при использовании летнего времени (DST), а значит, вы не получаете все преимущества поддержки часовых поясов. Кроме того, вы, вероятно, столкнётесь с некоторыми проблемами при сравнении абсолютных и относительных дат. Так как Django теперь предоставляет вам значения абсолютного времени, вы получите исключения везде, где вы сравниваете значения из моделей или форм со значениями относительного времени, которые вы создаёте в своем коде.
Вторым шагом будет рефакторинг вашего кода в местах, где вы создаёте значения времени, теперь они должны быть абсолютными. Это может быть сделано постепенно. В модуле django.utils.timezone определены несколько вспомогательных функций: now(), is_aware(), is_naive(), make_aware() и make_naive().
Наконец, чтобы помочь вам найти код, требующий обновления, Django выдает предупреждение, когда вы пытаетесь сохранить наивное значение даты и времени в базе данных:
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",
)
Фикстуры¶
При сериализации значений абсолютного времени добавляется смещение относительно UTC, подобно этому:
"2011-09-01T13:20:30+03:00"
Для значений относительного времени такого не происходит:
"2011-09-01T13:20:30"
Для моделей с полями DateTimeField такая разница в поведении делает невозможным создание фикстуры, которая может применяться в обоих режимах поддержки часовых поясов.
Фикстуры, созданные с USE_TZ = False или до Django 1.4, используют относительный формат. Если у вашего проекта есть такие фикстуры, то после включения поддержки часовых поясов, вы увидите исключения RuntimeWarning при попытке загрузить такие фикстуры. Чтобы избавиться от таких сообщений, вам надо преобразовать ваши фикстуры в абсолютный формат.
Вы можете сгенерировать фикстуры заново с помощью loaddata, а затем dumpdata. Или, если они имеют небольшой размер, вы можете просто отредактировать их, добавив смещение UTC, которое соответствует вашей TIME_ZONE, к каждому сериализованному объекту времени.
ЧаВО¶
Настройка¶
Мне не нужна поддержка часовых поясов. Должен ли я активировать их поддержку?
Да. Когда поддержка часового пояса включена, 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правильный часовой пояс для базы данных.Эти данные используются при подключении к базе данных, которая не поддерживает часовые зоны, при
USE_TZравномTrue.
Решение проблем¶
Моё приложение падает с
TypeError: can't compare offset-naiveand offset-aware datetimes– в чём проблема?Давайте воспроизведем эту ошибку, сравнив наивную и осознанную дату и время:
>>> 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
Если встречаетесь с этой ошибкой, наиболее вероятно, что ваш код сравнивает эти две вещи:
значение времени, предоставленное 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)при включенной поддержке часовых поясов – это плохо?При включенной поддержке часовых поясов, модуль для работы с базой данных ожидает получать только значения абсолютного времени от вашего кода. Это предупреждение показывает, что модуль получил значение относительного времени. То есть, вы не завершили портирование своего кода для поддержки часовых поясов. См. руководство по миграции для подсказок относительно этого процесса.
Тем временем, для обратной совместимости, предполагается, что по умолчанию значение времени создано в локальном часовом поясе. Это логично.
now.date()это вчера! (или завтра)Если вы всегда использовали относительное время, возможно вы уверены, что можете преобразовать значение даты/времени в дату с помощью его метода
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вdate. Например, вы можете использовать шаблонный фильтрdateдля отображения только даты. Этот фильтр учтёт текущий часовой пояс при преобразовании значения даты/времени и обеспечит правильное отображение результата.Если вам действительно нужно выполнить преобразование самостоятельно, вы должны сначала убедиться, что дата и время преобразуются в соответствующий часовой пояс. Usually, this will be the current timezone:
>>> 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)
Я получил ошибку «
Установлены ли pytz и поддержка часовых поясов в базе данных?» pytz установлен, я предполагаю, что проблема в базе данных?Если вы используете MySQL, смотрите раздел Определения часовых поясов о MySQL, там описан процесс установки поддержки часовых поясов для MySQL.
Использование¶
У меня есть строка
"2012-02-21 10:28:45"и я знаю, что она в часовом поясе"Europe/Helsinki". Как мне преобразовать её в значение абсолютного времени?Здесь вам нужно создать необходимый экземпляр ZoneInfo и прикрепить его к наивному 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 знает, как сравнивать значения абсолютного времени, принимая во внимание смещение 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=zoneinfo.ZoneInfo(key='Europe/Paris'))
В этом примере библиотека pytz_ установлена и параметр
TIME_ZONEнастроен на"Europe/Paris".Как я могу получить список всех доступных часовых поясов?
zoneinfo.available_timezones()предоставляет набор всех действительных ключей для часовых поясов IANA, доступных в вашей системе. Рекомендации по использованию см. в документации.