Часовые пояса

Введение

При включенной поддержке часовых поясов, Django сохраняет в базе данных информацию о дате и времени в формате UTC, использует специальные объекты абсолютного времени и переводит значения к часовому поясу конечного пользователя в шаблонах и формах.

Такая поддержка становится актуальной, если ваши пользователи живут в разных часовых поясах и требуется отображать информацию о дате и времени в соответствии с настенными часами каждого пользователя.

Даже если ваш сайт используется в пределах одного часового пояса, хорошей привычкой будет хранение данных о времени в формате UTC. Основной причиной этой рекомендации является летнее время (Daylight Saving Time, DST). Во многих странах используется летнее время, когда часы переводятся весной на час вперёд, а осенью назад. При работе с локальным временем, у вас проявляется вероятность получения ошибок из-за времени дважды в год, в момент перевода часов. (Документация на пакет pytz рассматривает эти вопросы очень подробно.) Это может не иметь значения для вашего блога, но это становится проблемой, когда вы обсчитываетесь в расчётах с клиентами дважды в год ежегодно. Решением этой проблемы является использование UTC в коде и преобразование времени к локальному при взаимодействии с конечным пользователем.

По умолчанию, поддержка часовых поясов отключена. Для её активации установите USE_TZ = True в файле конфигурации проекта. Установка пакета pytz крайне рекомендована, но не является обязательной. Это просто как:

$ sudo pip install pytz

Примечание

Файл конфигурации settings.py, созданный с помощью django-admin.py startproject для удобства включает в себя USE_TZ = True.

Примечание

Существует также независимый, но связанный параметр конфигурации USE_L10N, который управляет активацией форматов локализации. Обратитесь к разделу Формат локализации для получения подробностей.

Если вы бьётесь над определённой проблемой, начните с ЧаВо по часовым поясам.

Концепция

Объекты относительного и абсолютного времени

Объекты класса datetime.datetime обладают атрибутом tzinfo, который может быть использован для хранения информации о часовом поясе и представлен потомком класса datetime.tzinfo. Если этот атрибут установлен и описывает смещение, то объект описывает абсолютное время. Иначе речь идёт об относительном времени.

Вы можете использовать функции is_aware() и is_naive() для определения типа объекта.

При отключенной поддержке часовых поясов, Django использует относительное время. Такое поведение подходит для многих случаев. В этом режиме, для получения информации о текущем времени, потребуется сделать:

import datetime

now = datetime.datetime.now()

При включенной поддержке часовых поясов, Django использует абсолютное время. Если ваш код создаёт объекты даты-времени, они тоже должны быть представлены абсолютным временем. В этом режиме, предыдущий пример будет выглядеть так:

import datetime
from django.utils.timezone import utc

now = datetime.datetime.utcnow().replace(tzinfo=utc)

Примечание

Модуль django.utils.timezone предоставляет функцию now(), которая возвращает текущее время, относительное или абсолютное, в зависимости от значения параметра конфигурации USE_TZ.

Предупреждение

Работа с объектами абсолютного времени не всегда интуитивно понятна. Например, аргумент tzinfo не надёжно работает с часовыми поясами в летнем времени. Использование UTC обычно безопаснее. Если вы работаете с несколькими часовыми поясами, то тщательно изучите документацию на pytz.

Примечание

Объекты класса datetime.time также обладают атрибутом tzinfo, а PostgreSQL имеет соответствующий тип time with time zone. Тем не менее, как написано в документации на PostgreSQL, этот тип “обладает свойствами, которые имеют сомнительную полезность”.

Django поддерживает только объекты относительного времени и вызывает исключение, если вы попытаетесь сохранить объект абсолютного времени.

Интерпретация объектов относительного времени

Если параметр конфигурации USE_TZ установлен в True, Django продолжает принимать объекты относительного времени, чтобы обеспечить обратную совместимость. Когда слой базы данных получает такой объект, он пытается преобразовать его в объект абсолютного времени, интерпретируя его в часовом поясе по умолчанию и выбрасывает предупреждение.

К сожалению, из-за перехода на летнее время, некоторые объекты не могут быть преобразованы. В таких ситуациях, pytz вызывает исключение. Другие реализации tzinfo, такие как часовой пояс по умолчанию используемый, если pytz не установлен, могут вызывать исключение или возвращать неточные результаты. Именно поэтому вы должны всегда создавать объекты абсолютного времени, если включена поддержка часовых поясов.

На практике это редко является проблемой. Django предоставляет вам объекты абсолютного времени в моделях и формах и чаще всего, новые объекты даты создаются из уже имеющихся с помощью timedelta арифметики. Чаще всего в коде используется текущая дата и функция timezone.now () создаёт объект правильного типа.

Стандартный часовой пояс и текущий часовой пояс

Стандартный часовой пояс — это часовой пояс, определённый параметром конфигурации TIME_ZONE.

Текущая часовой пояс — часовой пояс для которого отображается страница сайта.

Вы должны настраивать текущий часовой пояс на пояс пользователя с помощью activate(). В противном случае будет использоваться стандартный часовой пояс.

Примечание

Как указано в документации на параметр TIME_ZONE, Django устанавливает переменные окружения так, что она работает в стандартном часовом поясе. Это происходит вне зависимости от значения переменой USE_TZ и текущего часового пояса.

Когда параметр USE_TZ установлен в True, полезно сохранять обратную совместимость с приложениями, которые все ещё работают с локальным временем. Тем не менее, как показано выше, это не совсем надёжно и вам следует всегда работать с величинами абсолютного времени (UTC) в вашем коде. Например, используйте utcfromtimestamp() вместо fromtimestamp() и не забудьте установить tzinfo в utc.

Выбор текущего часового пояса

Текущий часовой пояс соответствует текущей локали для переводов. Тем не менее, не существует аналога для HTTP заголовка Accept-Language, который Django может использовать для автоматического определения часового пояса пользователя. Вместо этого, Django предоставляет функции выбора часового пояса. Используйте их при построении логики выбора часового пояса.

Большинство сайтов, которые заботятся о часовых поясах, просто спрашивают пользователя о его часовом поясе и сохраняют эту информацию в профайле. Для анонимных пользователей, такие сайты используют часовой пояс основной аудитории или UTC. Библиотека pytz предоставляет шпаргалки, например списки часовых поясов для стран, которые можно использовать для начального наполнения интерфейса.

Ниже приведён пример сохранения текущего часового пояса в сессии. (Пример не использует обработку ошибок для простоты.)

Добавьте следующую строку в MIDDLEWARE_CLASSES:

from django.utils import timezone

class TimezoneMiddleware(object):
    def process_request(self, request):
        tz = request.session.get('django_timezone')
        if tz:
            timezone.activate(tz)
        else:
            timezone.deactivate()

Создайте представление, которое может установить текущий часовой пояс:

import pytz
from django.shortcuts import redirect, render

def set_timezone(request):
    if request.method == 'POST':
        request.session['django_timezone'] = pytz.timezone(request.POST['timezone'])
        return redirect('/')
    else:
        return render(request, 'template.html', {'timezones': pytz.common_timezones})

Добавьте форму в template.html, которая будет выполнять POST запрос к этому предоставлению:

{% load tz %}
<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="selected"{% endif %}>{{ tz }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set" />
</form>

Ввод значений абсолютного времени в формы

После активации поддержки часовых поясов, Django интерпретирует значения времени, вводимые в формы относительно текущего часового пояса, и возвращает объекты абсолютного времени в cleaned_data.

Если текущий часовой пояс выбрасывает исключение для значений времени, которые не существуют или не могут быть преобразованы из-за перехода на летнее время (часовые пояса, предоставляемые через pytz, так делают), то такие значения времени будут интерпретированы как неправильные.

Вывод объектов абсолютного времени в шаблонах

После активации поддержки временных зон, Django преобразует объекты абсолютного времени в текущий часовой пояс при отображении в шаблоне. Процесс идёт аналогично локализации форматирования.

Предупреждение

Django не преобразует объекты относительного времени, так как они могут иметь двусмысленное значение. Ваш код не должен создавать объекты относительного времени при включенной поддержке часовых поясов. Тем не менее, вы можете включить принудительную конвертацию с помощью шаблонных фильтров, описанных далее.

Преобразование к локальному времени не всегда полезно, так как данные могут генерироваться для компьютеров, а не для людей. Следующие фильтры и теги, предоставляемые шаблонной библиотекой тегов 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

При включенном контекстном процессоре django.core.context_processors.tz(), по умолчанию он включен, каждый контекст запроса RequestContext содержит переменную TIME_ZONE, которая предоставляет имя текущего часового пояса.

Если вы не используете класс RequestContext, то вы можете получить это значение с помощью тега get_current_timezone:

{% get_current_timezone as TIME_ZONE %}

Шаблонные фильтры

Эти фильтры работают с обоими типами объектов времени. Для обеспечения конвертации, они предполагают, что значения относительного времени представлены в текущем часовом поясе. Эти фильтры всегда возвращают значения абсолютного времени.

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. Часовой пояс базы данных будет установлен соответственно в TIME_ZONE или UTC, т.е. Django получает корректные значения времени в любом случае. Вам не требуется выполнять дополнительные преобразования.

Другие базы данных

Остальные бэкэнды сохраняют значения времени без информации о часовом поясе. Если вы переключите с 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, к каждому сериализованному объекту времени.

ЧаВО

Настройка

  1. Мне не нужна поддержка часовых поясов. Должен ли я активировать их поддержку?

    Да. Если поддержка часовых поясов активирована, то Django использует более точную модель локального времени. Это ограждает вас от коварных и невоспроизводимых ошибок, связанных летним временем (DST).

    В этом отношении часовые пояса сопоставимы с unicode у Python. Сначала это непросто понять. Вы сталкиваетесь с ошибками кодирования и декодирования. Затем изучаете правила. И некоторые проблемы исчезают. Вы больше не получаете странные строки, когда ваше приложение получает не ASCII строки.

    При активации поддержки часовых поясов, вы столкнетесь с некоторыми ошибками, потому что используете относительные значения даты/времени там, где Django ожидает абсолютные значения. Такие ошибки обнаруживаются при запуске тестов и их несложно исправить. Вы быстро разберётесь, как можно избежать неправильных операций.

    С другой стороны, ошибки, вызванные отсутствием наличия поддержки часовых поясов, намного сложнее предотвращать, диагностировать и исправлять. Любые задачи, включающие в себя планирование или действия с датами – явные кандидаты на подобные ошибки, которые будут портить вам жизнь дважды в год.

    Пр этим причинам поддержка часовых поясов активирована по умолчанию в новых проектах и вам не следует отключать её, если у вас нет серьёзных причин для этого.

  2. Я включил поддержку часовых поясов. Счастье наступило?

    Может быть. Вы лучше защищены от ошибок, связанных с летним временем, но всё ещё можете “выстрелить себе в ногу”, небрежно превращая относительное время в абсолютно и наоборот.

    Если ваше приложение соединяется с другими системами, например, если оно запрашивает данные у веб-сервиса, следует проверить, что значения времени определены должным образом. Для безопасной передачи значений времени, их представление должно содержать смещение относительно 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
    

    (Чтобы реализовать эту функцию, вы должны решить, какой из ответов 28.02.2011 или 01.03.2011 является результатом операции 29.02.2012 минус один год, что зависит от ваших бизнес-требований.)

  3. Должен ли я установить pytz?

    Да. Django придерживается политики не требовать внешних зависимостей и поэтому установка pytz не обязательна. Однако, правильнее будет установить эту библиотеку.

    Как только активируете поддержку часовых поясов, Django потребуется определение текущего часового пояса. При наличии pytz, Django загружает это определение из базы данных tz. Это самое точное решение. В противном случае, Django полагается на различие между местным временем и UTC, которое запрашивается у операционной системы, чтобы выполнять преобразования. Это менее надежно, особенно при наличии летнего времени.

    Кроме того, если требуется поддерживать пользователей в нескольких часовых поясах, pytz обеспечивает определения для часовых поясов.

Решение проблем

  1. Моё приложение падает с TypeError: can't compare offset-naive and offset-aware datetimes – в чём проблема?

    Давайте воспроизведём данную ошибку с помощью сравнения значений относительного и абсолютного времени:

    >>> import datetime
    >>> from django.utils import timezone
    >>> naive = datetime.datetime.utcnow()
    >>> aware = naive.replace(tzinfo=timezone.utc)
    >>> 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 когда потребуется.

  2. Я вижу множество RuntimeWarning: DateTimeField received a naive datetime (YYYY-MM-DD HH:MM:SS) при включенной поддержке часовых поясов – это плохо?

    При включенной поддержке часовых поясов, модуль для работы с базой данных ожидает получать только значения абсолютного времени от вашего кода. Это предупреждение показывает, что модуль получил значение относительного времени. То есть, вы не завершили портирование своего кода для поддержки часовых поясов. См. руководство по миграции для подсказок относительно этого процесса.

    Тем временем, для обратной совместимости, предполагается, что по умолчанию значение времени создано в локальном часовом поясе. Это логично.

  3. 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)
    

Использование

  1. У меня есть строка "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 для tzinfo API. Также, вы можете пожелать перехватывать исключение InvalidTimeError. Документация pytz содержит больше примеров. Вы должны ознакомиться с ней перед работой со значениями абсолютного времени.

  2. Как я могу получить текущее время в текущем часовом поясе?

    Хорошо, первым вопросом будет: вам действительно это надо?

    Вы должны использовать только локальное время при взаимодействии с людьми, для этого модуль шаблонов предоставляет фильтры и теги для преобразования значений времени в нужный часовой пояс.

    Более того, 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=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    

    В этом примере библиотека pytz установлена и параметр TIME_ZONE настроен на "Europe/Paris".

  3. Как я могу получить список всех доступных часовых поясов?

    Библиотека pytz предоставляет шпаргалки, которые содержат список текущих часовых поясов и список всех доступных часовых поясов. Некоторые из них представляют лишь исторический интерес.