Удивительно, но арифметика времени не обрабатывается должным образом с объектами Python, учитывающими часовой пояс. Например, рассмотрим это фрагмент, который создает объект с учетом часового пояса в 2022-10-30 02:00.
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
zone = ZoneInfo("Europe/Madrid")
HOUR = timedelta(hours=1)
u0 = datetime(2022, 10, 30, 2, tzinfo=zone)
В 2:59 часы снова перешли на 2:00, что ознаменовало окончание периода перехода на летнее время.
Это делает 2022-10-30 02:00 неоднозначным. 30 октября 2022 года часы дважды показывали 2:00. Кулак приходит
Экземпляр летнего времени 2022-10-30 02:00:00+02:00
, за которым следует экземпляр зимнего времени 2022-10-30 02:00:00+01:00
, когда часовой пояс сместился с CEST на CET.
Python решает эту неоднозначность, выбирая u0
в качестве первого из двух экземпляров, одного из DST.
интервал. Это подтверждается распечаткой u0
и названия часового пояса:
>>> print(u0)
2022-10-30 02:00:00+02:00
>>> u0.tzname()
'CEST'
это центральноевропейский часовой пояс, в котором действует летнее время.
Если мы добавим один час к u0
, переход в CET
, то есть зимний часовой пояс Центральной Европы, будет правильно определен.
>>> u1 = u0 + HOUR
>>> u1.tzname()
'CET'
Однако время не возвращается к 2:00, как ожидалось:
>>> print(u1)
2022-10-30 03:00:00+01:00
'CET'
Итак, если добавить интервал в 1 час, это будет выглядеть так, как будто прошло 2 часа. Один час из-за смещения времени на стене с 2:00 на
3:00 и еще один из-за смены часового пояса, который смещается на 1 час в сторону UTC. И наоборот, можно было бы ожидать u1
чтобы распечатать как 2022-10-30 02:00:00+01:00
. Этот двухчасовой сдвиг подтверждается преобразованием u0
и u1
в UTC:
>>> u0.astimezone(timezone.utc)
datetime.datetime(2022, 10, 30, 0, 0, tzinfo=datetime.timezone.utc)
>>> u1.astimezone(timezone.utc)
datetime.datetime(2022, 10, 30, 2, 0, tzinfo=datetime.timezone.utc)
Что еще хуже, временной интервал между u1
и u0
рассчитывается непоследовательно в зависимости от
выбранный часовой пояс. С одной стороны мы имеем:
>>> u1 - u0
datetime.timedelta(seconds=3600)
что эквивалентно интервалу в 1 час. С другой стороны, если мы проделаем тот же расчет в формате UTC:
>>> u1.astimezone(timezone.utc) - u0.astimezone(timezone.utc)
datetime.timedelta(seconds=7200)
расчетный интервал составляет 2 часа.
В заключение, оказывается, что обработка Python дельты времени в дата-времени с учетом часового пояса делает упор на локальное время часов, а не на последовательные логические интервалы, что приводит к потенциальным несоответствиям при пересечении границ летнего времени.
На мой взгляд, это может ввести в заблуждение, поскольку существование библиотеки zoneinfo
создает впечатление, что подобные проблемы имеют место быть.
решено.
Делает кто-нибудь знает, это ошибка или ожидаемое поведение? Кто-нибудь еще сталкивался с этой проблемой и как вы справляетесь с этими несоответствиями в своих приложениях? Если это ожидаемое поведение, возможно, документация Python должна содержать более четкие рекомендации или предупреждения о выполнении арифметики времени с объектами, учитывающими часовой пояс.
Я проверил описанное поведение на Python 3.11 и 3.12. Аналогичные результаты получены и для других часовых поясов, например. «Европа/Афины», но я не проводил тщательную проверку для всех часовых поясов.
Я согласен, что это ожидаемое поведение. Кажется, что невозможно установитьfold=1, просто добавив временную дельту к u0
. Если да, то какова цель атрибута fold
? Как вы думаете, это может быть ошибка Python?
Арифметика timedelta — это арифметика настенного времени, т. е. если вы добавляете/вычитаете время, дата и время ведут себя как настенные часы, не знающие активного/неактивного летнего времени. См. также публикацию в блоге Пола Ганссла об этой причуде.
@FObersteiner Спасибо за ответ. Это превосходное объяснение того, как работает дельта-арифметика, и отличное объяснение того, почему были приняты эти соглашения.
В дополнение к моему комментарию, вот пример использования fold
, который, возможно, поможет прояснить ситуацию (обратите внимание на слегка измененное время):
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
zone = ZoneInfo("Europe/Madrid")
HOUR = timedelta(hours=1)
u0 = datetime(2022, 10, 30, 1, 59, 59, tzinfo=zone)
print(u0+HOUR) # one hour later on a *wall clock*...
2022-10-30 02:59:59+02:00 # CEST
print((u0+HOUR).replace(fold=1)) # specify on which side of the "DST fold" we want to be...
2022-10-30 02:59:59+01:00 # CET
Вы можете использовать fold
, чтобы указать, должна ли дата и время приходиться на определенную сторону «сгиба» летнего времени.
Применяя арифметику timedelta Python, время на стене все равно не имеет значения:
print((u0+HOUR).replace(fold=1)-u0)
1:00:00
print((u0+HOUR)-u0)
1:00:00
Поэтому, если вы хотите, чтобы арифметика timedelta была арифметикой абсолютного времени, используйте UTC (как показано в OP).
Что действительно странно, так это пробелы в летнее время (несуществующие даты и время):
u0 = datetime(2022, 3, 27, 1, 59, 59, tzinfo=zone)
print(u0+timedelta(seconds=1))
2022-03-27 02:00:00+01:00 # non-existent datetime; should be 03:00 CEST
Опять же, чтобы обойти такие странности: используйте абсолютное добавление в формате UTC;
print((u0.astimezone(timezone.utc)+timedelta(seconds=1)).astimezone(zone))
2022-03-27 03:00:00+02:00
Вики-сайт dst tag содержит более подробную информацию и иллюстрации пробелов и складок летнего времени.
Возникает вопрос: почему арифметика даты и времени не обновляет атрибут fold
автоматически?
@MarkRansom (обновлено) В случае 1 / неоднозначного значения даты и времени я думаю, что это нормально («правильно» по его собственному определению), поскольку, добавляя 1-часовую дельту к настенным часам, вы фактически пропускаете временной интервал, в течение которого складка будет актуальной / Сделать разницу. В случае 2 / несуществующей даты и времени складка не имеет значения. Я не знаю, почему не реализована проверка, существует ли дата и время или нет.
Я предполагаю, что это связано с тем, что Python позволяет напрямую создавать несуществующие даты. Например. u = datetime(2022,3,27, 2, 30, tzinfo=zone)
бегает без нареканий.
Это странно... Я ожидал, что u1 будет в 2 часа ночи с
fold=1
.