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

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

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

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

Even if your website is available in only one time zone, it’s still good practice to store data in UTC in your database. The main reason is daylight saving time (DST). Many countries have a system of DST, where clocks are moved forward in spring and backward in autumn. If you’re working in local time, you’re likely to encounter errors twice a year, when the transitions happen. This probably doesn’t matter for your blog, but it’s a problem if you over bill or under bill your customers by one hour, twice a year, every year. The solution to this problem is to use UTC in the code and use local time only when interacting with end users.

Time zone support is disabled by default. To enable it, set USE_TZ = True in your settings file.

Примечание

В 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()

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

Dealing with aware datetime objects isn’t always intuitive. For instance, the tzinfo argument of the standard datetime constructor doesn’t work reliably for time zones with DST. Using UTC is generally safe; if you’re using other time zones, you should review the zoneinfo documentation carefully.

Примечание

Объекты 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 и текущего часового пояса.

When USE_TZ is True, this is useful to preserve backwards-compatibility with applications that still rely on local time. However, as explained above, this isn’t entirely reliable, and you should always work with aware datetimes in UTC in your own code. For instance, use fromtimestamp() and set the tz parameter to utc.

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

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

Most websites that care about time zones ask users in which time zone they live and store this information in the user’s profile. For anonymous users, they use the time zone of their primary audience or UTC. zoneinfo.available_timezones() provides a set of available timezones that you can use to build a map from likely locations to time zones.

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

Добавьте следующее промежуточное программное обеспечение в 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.

Converted datetimes that don’t exist or are ambiguous because they fall in a DST transition will be reported as invalid values.

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

Когда вы включаете поддержку часовых поясов, 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. Мне не нужно несколько часовых поясов. Должен ли я включить поддержку часовых поясов?

    Yes. When time zone support is enabled, Django uses a more accurate model of local time. This shields you from subtle and unreproducible bugs around daylight saving time (DST) transitions.

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

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

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

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

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

    If your application connects to other systems – for instance, if it queries a web service – make sure datetimes are properly specified. To transmit datetimes safely, their representation should include the UTC offset, or their values should be in UTC (or both!).

    Наконец, наша календарная система содержит интересные крайние случаи. Например, вы не всегда можете вычесть один год непосредственно из заданной даты:

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

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