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

Быстрый обзор

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

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

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

По умолчанию поддержка часовых поясов отключена. Чтобы включить ее, установите значение USE_TZ = True в файле настроек.

Примечание

В Django 5.0 поддержка часовых поясов будет включена по умолчанию.

Поддержка часовых поясов использует zoneinfo, который является частью стандартной библиотеки Python, начиная с Python 3.9. Пакет backports.zoneinfo автоматически устанавливается вместе с Django, если вы используете Python 3.8.

Примечание

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

Если вы боретесь с конкретной проблемой, начните с time zone FAQ.

Концепции

Наивные и осознанные объекты времени суток

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

Вы можете использовать is_aware() и is_naive(), чтобы определить, являются ли времена данных осознанными или наивными.

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

import datetime

now = datetime.datetime.now()

Когда поддержка часовых поясов включена (USE_TZ=True), Django использует объекты datetime с учетом часовых поясов. Если ваш код создает объекты datetime, они также должны быть осведомлены об этом. В этом режиме приведенный выше пример становится:

from django.utils import timezone

now = timezone.now()

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

Работа с объектами datetime не всегда интуитивно понятна. Например, аргумент tzinfo стандартного конструктора datetime не работает надежно для часовых поясов с DST. Использование UTC обычно безопасно; если вы используете другие часовые пояса, вам следует внимательно изучить документацию zoneinfo.

Примечание

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

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

Интерпретация наивных объектов времени суток

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

К сожалению, во время перехода на летнее время некоторые объекты времени не существуют или являются неоднозначными. Поэтому всегда следует создавать объекты datetime, когда включена поддержка часовых поясов. (Примеры использования атрибута Using ZoneInfo section of the zoneinfo docs для указания смещения, которое должно применяться к дате во время перехода на летнее время, см. в fold).

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

Часовой пояс по умолчанию и текущий часовой пояс

Часовой пояс по умолчанию - это часовой пояс, определяемый настройкой TIME_ZONE.

текущий часовой пояс - это часовой пояс, который используется для рендеринга.

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

Примечание

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

Когда USE_TZ равно True, это полезно для сохранения обратной совместимости с приложениями, которые все еще полагаются на местное время. Однако, as explained above, это не совсем надежно, и вы всегда должны работать с известным временем в UTC в своем собственном коде. Например, используйте fromtimestamp() и установите параметр tz в значение utc.

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

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

Большинство сайтов, заботящихся о часовых поясах, спрашивают пользователей, в каком часовом поясе они живут, и хранят эту информацию в профиле пользователя. Для анонимных пользователей используется часовой пояс их основной аудитории или 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 %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set">
</form>

Ввод данных о часовом поясе в формах

Когда вы включаете поддержку часовых поясов, Django интерпретирует даты, введенные в формах, в current time zone и возвращает осознанные объекты datetime в cleaned_data.

Преобразованные даты, которые не существуют или являются неоднозначными, поскольку попадают в переходный период DST, будут представлены как недопустимые значения.

Вывод в шаблонах с учетом часового пояса

Когда вы включаете поддержку часовых поясов, Django преобразует объекты datetime в current time zone, когда они отображаются в шаблонах. Это ведет себя очень похоже на format localization.

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

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

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

Теги шаблона

localtime

Включает или выключает преобразование известных объектов datetime в текущий часовой пояс в содержащемся блоке.

Этот тег имеет точно такие же эффекты, как и параметр 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.

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

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

localtime

Принудительное преобразование одного значения в текущий часовой пояс.

Например:

{% load tz %}

{{ value|localtime }}

utc

Принудительное преобразование одного значения в UTC.

Например:

{% load tz %}

{{ value|utc }}

timezone

Принудительное преобразование одного значения в произвольный часовой пояс.

Аргумент должен быть экземпляром подкласса tzinfo или именем часового пояса.

Например:

{% load tz %}

{{ value|timezone:"Europe/Paris" }}

Руководство по миграции

Вот как перенести проект, который был начат до того, как Django поддерживал часовые пояса.

База данных

PostgreSQL

Бэкенд PostgreSQL хранит время даты как timestamp with time zone. На практике это означает, что при хранении он преобразует время дат из часового пояса соединения в UTC, а при извлечении - из UTC в часовой пояс соединения.

Как следствие, если вы используете PostgreSQL, вы можете свободно переключаться между USE_TZ = False и USE_TZ = True. Часовой пояс соединения с базой данных будет установлен на TIME_ZONE или UTC соответственно, так что Django получит правильное время дат во всех случаях. Вам не нужно выполнять никаких преобразований данных.

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

Другие бэкенды хранят данные без информации о часовом поясе. Если вы переключаетесь с USE_TZ = False на USE_TZ = True, вы должны преобразовать ваши данные из местного времени в UTC - что не является детерминированным, если ваше местное время имеет DST.

Код

Первый шаг - добавить USE_TZ = True в файл настроек. На этом этапе все должно в основном работать. Если вы создаете наивные объекты datetime в своем коде, Django делает их известными, когда это необходимо.

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

Поэтому вторым шагом будет рефакторинг вашего кода везде, где вы инстанцируете объекты datetime, чтобы сделать их осознанными. Это можно сделать постепенно. 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при их загрузке. Чтобы избавиться от предупреждений, вы должны преобразовать ваши фикстуры в формат «aware».

Вы можете регенерировать фикстуры с помощью loaddata, затем dumpdata. Или, если они достаточно малы, вы можете отредактировать их, чтобы добавить смещение UTC, соответствующее вашему TIME_ZONE, к каждому сериализованному времени даты.

FAQ

Настройка

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

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

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

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

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

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

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

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

  3. Как взаимодействовать с базой данных, в которой хранятся даты в местном времени?

    Установите в параметре TIME_ZONE соответствующий часовой пояс для этой базы данных в параметре DATABASES.

    Это полезно для подключения к базе данных, которая не поддерживает часовые пояса и не управляется Django, когда USE_TZ становится True.

Устранение неполадок

  1. Мое приложение аварийно завершается с TypeError: can't compare offset-naive and offset-aware datetimes - что не так?

    Воспроизведем эту ошибку, сравнив наивный и осознанный datetime:

    >>> 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 по мере необходимости.

  2. Я вижу много RuntimeWarning: DateTimeField received a naive datetime (YYYY-MM-DD HH:MM:SS) while time zone support is active **- это плохо? **

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

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

  3. <<< 0 >> ** это вчера! (или завтра)**

    Если вы всегда пользовались наивными временами дат, вы, вероятно, считаете, что можно преобразовать время даты в дату, вызвав его метод 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)
    
  4. Я получаю ошибку «Are time zone definitions for your database installed?»

    Если вы используете MySQL, смотрите раздел Определения часовых поясов в примечаниях к MySQL для инструкций по загрузке определений часовых поясов.

Применение

  1. У меня есть строка "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'))
    
  2. Как я могу получить местное время в текущем часовом поясе?

    Первый вопрос - действительно ли вам это нужно?

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

    Кроме того, 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".

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

    zoneinfo.available_timezones() предоставляет набор всех допустимых ключей для временных зон IANA, доступных для вашей системы. Соображения по использованию см. в документации.

Вернуться на верх