Spec-Zone.ru › Django 5.1

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

Обзор

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

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

Даже если ваш сайт доступен только в одном часовом поясе, всё равно рекомендуется хранить данные в формате UTC в вашей базе данных. Главная причина — летнее время (ЛЗТ). Многие страны имеют систему ЛЗТ, где часы переводятся вперёд весной и назад осенью. Если вы работаете с местным временем, вам, скорее всего, придётся столкнуться с ошибками дважды в год, когда происходят переходы. Возможно, это не имеет значения для вашего блога, но это проблема, если вы пересчитываете или недосчитываете плату за услуги на час дважды в год, каждый год. Решением этой проблемы является использование UTC в коде и использование местного времени только при взаимодействии с конечными пользователями.

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

Изменено в Django 5.0:

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

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

Если у вас возникла проблема, начните с ЧАВО по часовым поясам.

Концепции

Простые и осознанные объекты datetime

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

Вы можете использовать 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 не работает надёжно для часовых поясов с ЛЗТ. Использование UTC, как правило, безопасно; если вы используете другие часовые пояса, вы должны внимательно изучить документацию по zoneinfo.

Примечание

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

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

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

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

К сожалению, во время переходов на летнее время некоторые даты и время не существуют или являются неоднозначными. Вот почему вы всегда должны создавать осознанные объекты datetime, когда включена поддержка часовых поясов. (См. Using ZoneInfo section of the zoneinfo docs для примеров использования атрибута fold для указания смещения, которое должно применяться к 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. zoneinfo.available_timezones() предоставляет набор доступных часовых поясов, которые можно использовать для построения карты вероятных расположений часовых поясов.

Вот пример, который сохраняет текущий часовой пояс в сессии. (Он полностью пропускает обработку ошибок ради простоты.)

Добавьте следующий middleware в 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 интерпретирует даты и время, введённые в формах, в текущем часовом поясе и возвращает осознанные объекты datetime в cleaned_data. Преобразованные даты и время, которые не существуют или являются неоднозначными из-за перехода на летнее время, будут сообщены как недопустимые значения.

Часовой пояс в шаблонах

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

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

Django не преобразует простые объекты 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.

Фильтры шаблонов

Эти фильтры принимают как aware, так и naive значения datetime. Для преобразования они предполагают, что naive значения datetime находятся в стандартной временной зоне. Они всегда возвращают aware значения 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. Временная зона подключения к базе данных будет установлена в DATABASE-TIME_ZONE или UTC соответственно, чтобы Django в любом случае получал правильные значения datetime. Вам не нужно выполнять какие-либо преобразования данных.

Настройки временной зоны

Настройка time zone для подключения в настройке DATABASES отличается от общей настройки TIME_ZONE.

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

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

Код

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

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

Поэтому второй шаг — переписать ваш код, где вы создаёте объекты datetime, чтобы сделать их aware. Это можно сделать постепенно. django.utils.timezone определяет некоторые полезные вспомогательные функции для кода совместимости: now(), is_aware(), is_naive(), make_aware() и make_naive().

Наконец, чтобы помочь вам найти код, который нужно обновить, Django выводит предупреждение, когда вы пытаетесь сохранить naive 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",
)

Файлы данных

При сериализации aware datetime смещение UTC включается, например:

"2011-09-01T13:20:30+03:00"

В то время как для naive datetime его нет:

"2011-09-01T13:20:30"

Для моделей с полями DateTimeField эта разница делает невозможным создание файла данных, который будет работать как с поддержкой временных зон, так и без неё.

Файлы данных, сгенерированные с помощью USE_TZ = False, или до Django 1.4, используют формат «naive». Если ваш проект содержит такие файлы данных, после включения поддержки временных зон вы увидите RuntimeWarning при их загрузке. Чтобы избавиться от предупреждений, вы должны преобразовать ваши файлы данных в формат «aware».

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

Вопросы и ответы

Настройка

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

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

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

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

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

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

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

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

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

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

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

  1. Моё приложение падает с TypeError: can't compare offset-naive and 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 по мере необходимости.

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

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

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

  3. 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)
    
  4. У меня ошибка “Are time zone definitions for your database installed?”

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

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

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

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

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

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

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/topics/i18n/timezones/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API