Два способа создания объектов 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
.