Два способа создания объектов datetime с учетом часовых поясов (Django). Разница в семь минут?

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

Но это не так:

import datetime

from django.utils.timezone import make_aware, get_current_timezone

make_aware(datetime.datetime(1999, 1, 1, 0, 0, 0), get_current_timezone())

datetime.datetime(1999, 1, 1, 0, 0, 0, tzinfo=get_current_timezone())
datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)

datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>)

В Django Admin GUI второй способ создает это (немецкий формат даты дд.мм.гггг):

01.01.1999 00:07:00

Почему разница в 7 минут, если я использую это:

datetime.datetime(1999, 1, 1, 0, 0, 0, tzinfo=get_current_timezone())

Это происходит на Django 3.2 и ниже, которые полагаются на библиотеку pytz. В Django 4 (если вы не включите настройку для использования устаревшей библиотеки), вывод двух приведенных вами примеров идентичен.

В Django 3.2 и ниже разница возникает из-за того, что локализованное время строится двумя разными способами. При использовании make_aware это делается путем вызова метода localize() на экземпляре pytz часового пояса. Во второй версии это делается путем передачи объекта tzinfo непосредственно в конструктор datetime.

Разница между ними хорошо показана в этой записи в блоге:

Самая большая ошибка, которую допускают люди при работе с pytz - это простое присоединение его часовых поясов к конструктору, поскольку это стандартный способ добавления часового пояса к дате в Python. Если вы попытаетесь сделать это, в лучшем случае вы получите нечто явно абсурдное:

import pytz
from datetime import datetime

NYC = pytz.timezone('America/New_York')
dt = datetime(2018, 2, 14, 12, tzinfo=NYC)
print(dt)
# 2018-02-14 12:00:00-04:56

Почему смещение времени -04:56, а не -05:00? Потому что это было местное среднее солнечное время в Нью-Йорке до принятия стандартизированных часовых поясов, и поэтому это первая запись в часовом поясе America/New_York. Почему pytz возвращает это значение? Потому что в отличие от модели стандартной библиотеки, в которой информация о часовом поясе вычисляется лениво, pytz использует подход нетерпеливого вычисления.

.

Каждый раз, когда вы конструируете знающий дататайм из наивного, вам нужно вызвать функцию localize:

dt = NYC.localize(datetime(2018, 2, 14, 12))
print(dt)
# 2018-02-14 12:00:00-05:00

Точно то же самое происходит в вашем примере Europe/Berlin. pytz охотно берет первую запись в своей базе данных, которая представляет собой солнечное время до 1983 года, которое опережало на 53 минуты и 28 секунд среднее время по Гринвичу (GMT). Это явно неуместно, учитывая дату - но tzinfo не знает, какую дату вы используете, пока вы не передадите ее localize().

В этом и заключается разница между двумя вашими подходами. Использование make_aware правильно вызывает localize() на объекте. Присвоение tzinfo непосредственно объекту datetime, однако, не делает этого, и в результате pytz использует (неправильную) информацию о часовом поясе, потому что это была просто первая запись для этого пояса в его базе данных.

В документации pytz косвенно упоминается и об этом:

Эта библиотека поддерживает только два способа построения локализованного времени. Первый заключается в использовании метода localize(), предоставляемого библиотекой pytz. Он используется для локализации наивного времени (времени без информации о часовом поясе)... Второй способ построения локализованного времени - преобразование существующего локализованного времени с помощью стандартного метода astimezone()... К сожалению, использование аргумента tzinfo в стандартных конструкторах datetime ''не работает'' с pytz для многих часовых поясов.

Именно из-за этих и некоторых других ошибок в pytz реализации Django отказался от него в пользу встроенного в Python zoneinfo модуля .

Больше из этой статьи блога:

На момент создания pytz был продуман для оптимизации производительности и корректности, но с изменениями, внесенными в PEP 495 и улучшением производительности dateutil, причины для его использования уменьшаются. ... Самой большой причиной использовать dateutil вместо pytz является тот факт, что dateutil использует стандартный интерфейс, а pytz - нет, и в результате этого очень легко использовать pytz неправильно.

Передача объекта pytz tzinfo непосредственно конструктору datetime является неправильной. Вы должны вызвать localize() на классе tzinfo, передав ему дату. Правильный способ инициализации времени даты в вашем втором примере следующий:

> berlin = get_current_timezone()
> berlin.localize(datetime.datetime(1999, 1, 1, 0, 0, 0))
datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)

... что соответствует тому, что производит make_aware.

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