Часовые пояса
Обзор
Если поддержка часовых поясов включена, Django сохраняет сведения о дате и времени в базе данных в формате UTC, внутри использует объекты даты и времени с учётом часового пояса, а в формах преобразует их в часовой пояс конечного пользователя. В шаблонах используется часовой пояс по умолчанию, но его можно изменить на часовой пояс конечного пользователя с помощью фильтров и тегов.
Это удобно, если ваши пользователи живут в разных часовых поясах и вы хотите отображать дату и время в соответствии с местным временем каждого пользователя.
Даже если ваш сайт доступен только в одном часовом поясе, всё равно рекомендуется хранить данные в базе в формате UTC. Главная причина — переход на летнее время (DST). Во многих странах действует система перехода на летнее время: весной часы переводят вперёд, а осенью — назад. Если вы работаете с местным временем, то, скорее всего, дважды в год при переходе будете сталкиваться с ошибками. Для вашего блога это, вероятно, не имеет значения, но становится проблемой, если вы дважды в год, каждый год выставляете клиентам счёт на час больше или меньше. Решение этой проблемы — использовать UTC в коде и местное время только при взаимодействии с конечными пользователями.
Поддержка часовых поясов включена по умолчанию. Чтобы отключить её, задайте USE_TZ =
False в файле настроек.
Поддержка часовых поясов использует zoneinfo, входящий в стандартную библиотеку Python начиная с Python 3.9.
Если вы столкнулись с конкретной проблемой, начните с раздела часто задаваемых вопросов о часовых поясах.
Понятия
Наивные объекты даты и времени и объекты с часовым поясом
Объекты Python 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()
Предупреждение
Работа с объектами даты и времени с учётом часового пояса не всегда интуитивно понятна. Например, аргумент tzinfo стандартного конструктора даты и времени ненадёжно работает для часовых поясов с переходом на летнее время. Использовать UTC, как правило, безопасно; если вы используете другие часовые пояса, внимательно изучите документацию zoneinfo.
Примечание
Объекты Python datetime.time также имеют атрибут tzinfo, а в PostgreSQL есть соответствующий тип time with time zone. Однако, как сказано в документации PostgreSQL, этот тип «обладает свойствами, из-за которых его практическая полезность сомнительна».
Django поддерживает только наивные объекты времени и вызывает исключение, если вы пытаетесь сохранить объект времени с часовым поясом, поскольку часовой пояс для времени без связанной с ним даты не имеет смысла.
Интерпретация наивных объектов даты и времени
Когда значение USE_TZ равно True, Django по-прежнему принимает наивные объекты даты и времени для обеспечения обратной совместимости. Получив такой объект, уровень базы данных пытается сделать его осведомлённым о часовом поясе, интерпретируя его в часовом поясе по умолчанию, и выдаёт предупреждение.
К сожалению, во время перехода на летнее время некоторые даты и время не существуют или неоднозначны. Поэтому при включённой поддержке часовых поясов всегда создавайте объекты даты и времени с учётом часового пояса. (В Using ZoneInfo section of the zoneinfo
docs приведены примеры использования атрибута 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:
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 или названием часового пояса.
Например:
{% load tz %}
{{ value|timezone:"Europe/Paris" }}
Руководство по миграции
Ниже описано, как перенести проект, созданный до появления поддержки часовых поясов в Django.
База данных
PostgreSQL
Бэкенд PostgreSQL хранит даты и время как timestamp with time zone. На практике это означает, что при сохранении он преобразует даты и время из часового пояса подключения в UTC, а при получении — из UTC в часовой пояс подключения.
Поэтому при использовании PostgreSQL можно свободно переключаться между USE_TZ
= False и USE_TZ = True. Часовой пояс подключения к базе данных будет установлен в UTC или DATABASE-TIME_ZONE соответственно, чтобы Django получал корректные даты и время в любом случае. Преобразовывать данные не нужно.
Другие базы данных
Другие бэкенды хранят даты и время без сведений о часовом поясе. Если вы переключитесь с USE_TZ = False на USE_TZ = True, необходимо преобразовать данные из местного времени в UTC — это невозможно выполнить однозначно, если в вашем часовом поясе действует переход на летнее время.
Код
Первый шаг — добавить USE_TZ = True в файл настроек. После этого всё должно в основном работать. Если в коде создаются наивные объекты даты и времени, Django при необходимости преобразует их в объекты с учётом часового пояса.
Однако такое преобразование может завершиться ошибкой во время перехода на летнее время, а значит, вы ещё не используете все преимущества поддержки часовых поясов. Кроме того, вероятно, вы столкнётесь с некоторыми проблемами, поскольку сравнивать наивные объекты даты и времени с объектами, учитывающими часовой пояс, невозможно. Теперь 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. Выбор зависит от требований вашего бизнеса.
-
Как взаимодействовать с базой данных, в которой даты и время хранятся в местном времени?
Задайте в параметре
DATABASESзначение параметраTIME_ZONE, соответствующее часовому поясу этой базы данных.Это полезно при подключении к базе данных, которая не поддерживает часовые пояса и не управляется Django, когда
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)while time zone support is active— это плохо?При включённой поддержке часовых поясов уровень базы данных ожидает, что ваш код будет передавать только даты и время с учётом часового пояса. Это предупреждение появляется, когда получена наивная дата и время. Оно означает, что перенос кода для поддержки часовых поясов ещё не завершён. Советы по этому процессу приведены в руководстве по миграции.
Пока что для обеспечения обратной совместимости дата и время считаются заданными в часовом поясе по умолчанию, что обычно и ожидается.
-
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, чтобы отобразить только дату из объекта даты и времени. Этот фильтр преобразует дату и время в текущий часовой пояс перед форматированием, гарантируя корректное отображение результата.Если вам всё же нужно выполнить преобразование самостоятельно, сначала преобразуйте дату и время в соответствующий часовой пояс. Обычно это будет текущий часовой пояс:
>>> 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". Как преобразовать её в дату и время с учётом часового пояса?Вам нужно создать экземпляр
ZoneInfoи присоединить его к наивной дате и времени:>>> 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. Поэтому в большинстве случаев достаточно даты и времени в 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'))
В этом примере текущий часовой пояс —
"Europe/Paris". -
Как посмотреть все доступные часовые пояса?
zoneinfo.available_timezones()предоставляет набор всех допустимых ключей часовых поясов IANA, доступных в вашей системе. Рекомендации по использованию см. в документации.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/topics/i18n/timezones/