Часовые пояса
Обзор
При включенном режиме поддержки часовых поясов Django сохраняет информацию о датах и временах в формате UTC в базе данных, использует объекты datetime с учетом часового пояса внутри и переводит их в часовой пояс конечного пользователя в шаблонах и формах.
Это удобно, если ваши пользователи проживают в нескольких часовых поясах, и вы хотите отображать информацию о датах и временах в соответствии с часовым поясом каждого пользователя.
Даже если ваш сайт доступен только в одном часовом поясе, все равно рекомендуется хранить данные в формате UTC в вашей базе данных. Главная причина — летнее время (DST). Многие страны имеют систему летнего времени, когда стрелки часов переводились вперёд весной и назад осенью. Если вы работаете с местным временем, вы, вероятно, столкнетесь с ошибками дважды в год, когда происходят переходы. (Документация pytz подробно обсуждает эти проблемы.) Это, вероятно, не важно для вашего блога, но это проблема, если вы переплачиваете или недоплачиваете клиентам на один час дважды в год, каждый год. Решение этой проблемы — использование UTC в коде и использование местного времени только при взаимодействии с конечными пользователями.
Поддержка часовых поясов по умолчанию отключена. Чтобы включить её, установите USE_TZ =
True в вашем файле настроек. Поддержка часовых поясов использует pytz, который устанавливается при установке Django.
Более старые версии не требуют pytz или не устанавливают его автоматически.
Примечание
Файл по умолчанию settings.py созданный django-admin
startproject включает USE_TZ = True для удобства.
Примечание
Также существует независимая, но связанная настройка USE_L10N, которая управляет тем, активирует ли Django локализацию форматов. Смотрите Локализация форматов для получения дополнительной информации.
Если у вас возникла конкретная проблема, начните с ЧАВО по часовым поясам.
Концепции
Простые и полные объекты datetime
Объекты datetime.datetime Python имеют атрибут tzinfo, который можно использовать для хранения информации о часовом поясе, представленной в виде экземпляра подкласса datetime.tzinfo. Когда этот атрибут установлен и описывает смещение, объект datetime является полным. В противном случае он простой.
Вы можете использовать is_aware() и is_naive() для определения того, являются ли datetime простыми или полными.
Когда поддержка часовых поясов отключена, 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 в MIDDLEWARE:
import pytz
from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin
class TimezoneMiddleware(MiddlewareMixin):
def process_request(self, request):
tzname = request.session.get('django_timezone')
if tzname:
timezone.activate(pytz.timezone(tzname))
else:
timezone.deactivate()
Создайте представление, которое может установить текущий часовой пояс:
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.
Если текущий часовой пояс вызывает исключение для дат и времён, которые не существуют или являются неоднозначными, потому что они попадают на переход на летнее время (часовые пояса, предоставляемые 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 хранит объекты datetime в формате timestamp with time zone. На практике это означает, что он преобразует объекты datetime из часового пояса подключения в UTC при сохранении и из UTC в часовой пояс подключения при извлечении.
Следовательно, если вы используете PostgreSQL, вы можете свободно переключаться между USE_TZ
= False и USE_TZ = True. Часовой пояс соединения с базой данных будет установлен на TIME_ZONE или UTC соответственно, чтобы Django получал корректные объекты datetime во всех случаях. Вам не нужно выполнять никаких преобразований данных.
Другие базы данных
Другие бэкэнды хранят объекты datetime без информации о часовом поясе. Если вы переключаетесь с USE_TZ = False на USE_TZ = True, вы должны преобразовать ваши данные из местного времени в UTC — что не является детерминированным, если местное время имеет летнее время.
Код
Первый шаг — добавить 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, к каждому сериализованному объекту datetime.
Вопросы и ответы
Настройка
-
Мне не нужны несколько часовых поясов. Нужно ли включать поддержку часовых поясов?
Да. Когда включена поддержка часовых поясов, Django использует более точную модель местного времени. Это защищает вас от скрытых и невоспроизводимых ошибок, связанных с переходом на летнее время (DST).
В этом отношении часовые пояса сравнимы с
unicodeв Python. Сначала это сложно. Вы получаете ошибки кодирования и декодирования. Затем вы учитесь правилам. И некоторые проблемы исчезают — у вас больше не будет искажённого вывода, когда ваше приложение получает не-ASCII вход.Когда вы включаете поддержку часовых поясов, вы столкнётесь с некоторыми ошибками, потому что вы используете объекты datetime без учёта часового пояса, где Django ожидает объекты datetime с учётом часового пояса. Такие ошибки появляются при запуске тестов, и их легко исправить. Вы быстро научитесь избегать некорректных операций.
С другой стороны, ошибки, вызванные отсутствием поддержки часовых поясов, гораздо сложнее предотвратить, диагностировать и исправить. Всё, что связано с планируемыми задачами или арифметикой datetime, является кандидатом на скрытые ошибки, которые будут проявляться только раз или два в год.
По этим причинам поддержка часовых поясов включена по умолчанию в новых проектах, и вы должны её оставлять, если у вас нет веской причины этого не делать.
-
Я включил поддержку часовых поясов. Я в безопасности?
Возможно. Вы лучше защищены от ошибок, связанных с переходом на летнее время, но вы всё ещё можете навредить себе, неосторожно превращая объекты datetime без учёта часового пояса в объекты datetime с учётом часового пояса и наоборот.
Если ваше приложение подключается к другим системам — например, если оно выполняет запрос к веб-сервису — убедитесь, что объекты datetime правильно указаны. Для безопасной передачи объектов datetime их представление должно включать смещение UTC или их значения должны быть в UTC (или оба!).
Наконец, наша календарная система содержит интересные ловушки для компьютеров:
>>> import datetime >>> def one_year_before(value): # DON'T DO THAT! ... 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, что зависит от ваших бизнес-требований.)
-
Как взаимодействовать с базой данных, которая хранит объекты datetime в местном времени?
Установите опцию
TIME_ZONEна соответствующий часовой пояс для этой базы данных в настройкеDATABASES.Это полезно для подключения к базе данных, которая не поддерживает часовые пояса и не управляется Django, когда
USE_TZнаходится вTrue.
Поиск и устранение неисправностей
-
Моё приложение падает с
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 just 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 предоставляет помощники, включая список текущих часовых поясов и список всех доступных часовых поясов – некоторые из которых представляют только исторический интерес.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.11/topics/i18n/timezones/