• 3.1
  • 5.0
  • 6.1
  • Версия документации: 3.2

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

Обзор

При включенной поддержке часовых поясов, 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. (The pytz documentation discusses these issues in greater detail.) 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. By default, time zone support uses pytz, which is installed when you install Django; Django also supports the use of other time zone implementations like zoneinfo by passing tzinfo objects directly to functions in django.utils.timezone.

Changed in Django 3.2:

Support for non-pytz timezone implementations was added.

Примечание

The default settings.py file created by django-admin startproject includes USE_TZ = True for convenience.

Примечание

There is also an independent but related USE_L10N setting that controls whether Django should activate format localization. See Формат локализации for more details.

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

Концепция

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

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

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

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

import datetime

now = datetime.datetime.now()

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

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 pytz documentation carefully.

Примечание

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

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

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

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

Unfortunately, during DST transitions, some datetimes don’t exist or are ambiguous. In such situations, pytz raises an exception. That’s why you should always create aware datetime objects when time zone support is enabled.

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

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

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

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. pytz provides helpers, like a list of time zones per country, that you can use to pre-select the most likely choices.

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

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

import pytz

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(pytz.timezone(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)

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

from django.shortcuts import redirect, render

def set_timezone(request):
    if request.method == 'POST':
        request.session['django_timezone'] = request.POST['timezone']
        return redirect('/')
    else:
        return render(request, 'template.html', {'timezones': pytz.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 tz in timezones %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set">
</form>

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

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

If the current time zone raises an exception for datetimes that don’t exist or are ambiguous because they fall in a DST transition (the timezones provided by pytz do this), such datetimes will be reported as invalid values.

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

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

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

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

Преобразование к локальному времени не всегда полезно, так как данные могут генерироваться для компьютеров, а не для людей. Следующие фильтры и теги, предоставляемые шаблонной библиотекой тегов tz, позволяют вам управлять преобразованиями объектов времени.

Шаблонные теги

localtime

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

Этот тег имеет точно такой же эффект, как и параметр USE_TZ, только для шаблонного движка. Он позволяет обеспечить более тонкий контроль над преобразованиями.

To activate or deactivate conversion for a template block, use:

{% 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

You can get the name of the current time zone using the get_current_timezone tag:

{% get_current_timezone as TIME_ZONE %}

Альтернативно, вы можете активировать контекстный процессор tz() и использовать контекстную переменную TIME_ZONE.

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

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

localtime

Обеспечивает конвертацию единственного значения в текущий часовой пояс.

Например:

{% load tz %}

{{ value|localtime }}

utc

Преобразовывает единственное значение к UTC.

Например:

{% load tz %}

{{ value|utc }}

timezone

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

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

Например:

{% load tz %}

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

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

Здесь описано как провести миграцию старого проекта для поддержки часовых поясов.

База данных

PostgreSQL

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

As a consequence, if you’re using PostgreSQL, you can switch between USE_TZ = False and USE_TZ = True freely. The database connection’s time zone will be set to TIME_ZONE or UTC respectively, so that Django obtains correct datetimes in all cases. You don’t need to perform any data conversions.

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

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

Код

Первым шагом надо добавить USE_TZ = True в конфигурационный файл и установить pytz (по возможности). После этого почти всё должно заработать. Если вы создаёте объекты относительного времени в вашем коде, Django будет преобразовывать их в объекты абсолютного времени при необходимости.

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

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

Finally, in order to help you locate code that needs upgrading, Django raises a warning when you attempt to save a naive datetime to the database:

RuntimeWarning: DateTimeField ModelName.field_name received a naive
datetime (2012-01-01 00:00:00) while time zone support is active.

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

import warnings
warnings.filterwarnings(
    'error', r"DateTimeField .* received a naive datetime",
    RuntimeWarning, r'django\.db\.models\.fields',
)

Фикстуры

При сериализации значений абсолютного времени добавляется смещение относительно UTC, подобно этому:

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

Для значений относительного времени такого не происходит:

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

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

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

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

ЧаВО

Настройка

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

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

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

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

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

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

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

    Если ваше приложение подключается к другим системам (например, если оно запрашивает веб-службу), убедитесь, что дата и время указаны правильно. Чтобы безопасно передавать дату и время, их представление должно включать смещение UTC, или их значения должны быть в формате UTC (или и то, и другое!).

    Finally, our calendar system contains interesting edge cases. For example, you can’t always subtract one year directly from a given date:

    >>> 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 правильный часовой пояс для базы данных.

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

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

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

    Let’s reproduce this error by comparing a naive and an aware 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) при включенной поддержке часовых поясов – это плохо?

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

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

  3. now.date() это вчера! (или завтра)

    Если вы всегда использовали относительное время, возможно вы уверены, что можете преобразовать значение даты/времени в дату с помощью его метода date(). Вы также можете считать, что date очень похож на datetime, за исключением его точности.

    None of this is true in a time zone aware environment:

    >>> import datetime
    >>> import pytz
    >>> paris_tz = pytz.timezone("Europe/Paris")
    >>> new_york_tz = pytz.timezone("America/New_York")
    >>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30))
    # This is the correct way to convert between time zones with pytz.
    >>> new_york = new_york_tz.normalize(paris.astimezone(new_york_tz))
    >>> paris == new_york, paris.date() == new_york.date()
    (True, False)
    >>> paris - new_york, paris.date() - new_york.date()
    (datetime.timedelta(0), datetime.timedelta(1))
    >>> paris
    datetime.datetime(2012, 3, 3, 1, 30, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    >>> new_york
    datetime.datetime(2012, 3, 2, 19, 30, tzinfo=<DstTzInfo 'America/New_York' EST-1 day, 19:00:00 STD>)
    

    Как показывает данный пример, одинаковые значения даты/времени имеют различные даты в зависимости от часового пояса, в котором они представлены. Но настоящая проблема глубже.

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

    К чему это приводит на практике?

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

    If you really need to do the conversion yourself, you must ensure the datetime is converted to the appropriate time zone first. Usually, this will be the current timezone:

    >>> from django.utils import timezone
    >>> timezone.activate(pytz.timezone("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()
    # Again, this is the correct way to convert between time zones with pytz.
    >>> local = current_tz.normalize(paris.astimezone(current_tz))
    >>> local
    datetime.datetime(2012, 3, 3, 8, 30, tzinfo=<DstTzInfo 'Asia/Singapore' SGT+8:00:00 STD>)
    >>> local.date()
    datetime.date(2012, 3, 3)
    
  4. Я получил ошибку «Установлены ли pytz и поддержка часовых поясов в базе данных?» pytz установлен, я предполагаю, что проблема в базе данных?

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

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

  1. У меня есть строка "2012-02-21 10:28:45" и я знаю, что она в часовом поясе "Europe/Helsinki" . Как мне преобразовать её в значение абсолютного времени?

    This is exactly what pytz is for.

    >>> from django.utils.dateparse import parse_datetime
    >>> naive = parse_datetime("2012-02-21 10:28:45")
    >>> import pytz
    >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None)
    datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=<DstTzInfo 'Europe/Helsinki' EET+2:00:00 STD>)
    

    Note that localize is a pytz extension to the tzinfo API. Also, you may want to catch pytz.InvalidTimeError. The documentation of pytz contains more examples. You should review it before attempting to manipulate aware datetimes.

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

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

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

    Более того, Python знает, как сравнивать значения абсолютного времени, принимая во внимание смещение UTC когда это требуется. Так гораздо проще (и значительно быстрее) создавать код для всех ваших моделей и представлений. Таким образом, в большинстве случаев времени в UTC, возвращённое функцией django.utils.timezone.now(), будет достаточно.

    For the sake of completeness, though, if you really want the local time in the current time zone, here’s how you can obtain it:

    >>> from django.utils import timezone
    >>> timezone.localtime(timezone.now())
    datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    

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

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

    pytz provides helpers, including a list of current time zones and a list of all available time zones – some of which are only of historical interest. zoneinfo also provides similar functionality via zoneinfo.available_timezones().

Back to Top