Spec-Zone.ru › Django 5.0

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

Обзор

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

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

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

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

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

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

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

Если у вас возникла проблема, начните с часто задаваемых вопросов по часовым поясам.

Концепции

Неявные и явные объекты 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 обычно безопасно; если вы используете другие часовые пояса, внимательно прочитайте документацию 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.

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

Эти фильтры принимают как объекты 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",
)

Файлы fixtures

При сериализации datetime с учётом часового пояса смещение UTC включается, как это:

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

В то время как для datetime без учёта часового пояса это не так:

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

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

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

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

ЧАВО по часовым поясам

Настройка

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

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

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

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

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

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

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

    Если ваше приложение подключается к другим системам – например, если оно запрашивает веб-сервис – убедитесь, что 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.0/topics/i18n/timezones/

Spec-Zone.ru

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