Несоответствия в арифметике времени с объектами DateTime с учетом часового пояса в Python

Удивительно, но арифметика времени не обрабатывается должным образом с объектами 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. Аналогичные результаты получены и для других часовых поясов, например. «Европа/Афины», но я не проводил тщательную проверку для всех часовых поясов.

Это странно... Я ожидал, что u1 будет в 2 часа ночи с fold=1.

user2357112 29.04.2024 12:58

Я согласен, что это ожидаемое поведение. Кажется, что невозможно установитьfold=1, просто добавив временную дельту к u0. Если да, то какова цель атрибута fold? Как вы думаете, это может быть ошибка Python?

zap 29.04.2024 13:25

Арифметика timedelta — это арифметика настенного времени, т. е. если вы добавляете/вычитаете время, дата и время ведут себя как настенные часы, не знающие активного/неактивного летнего времени. См. также публикацию в блоге Пола Ганссла об этой причуде.

FObersteiner 29.04.2024 13:53

@FObersteiner Спасибо за ответ. Это превосходное объяснение того, как работает дельта-арифметика, и отличное объяснение того, почему были приняты эти соглашения.

zap 29.04.2024 14:04
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
5
4
105
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В дополнение к моему комментарию, вот пример использования 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 автоматически?

Mark Ransom 29.04.2024 14:14

@MarkRansom (обновлено) В случае 1 / неоднозначного значения даты и времени я думаю, что это нормально («правильно» по его собственному определению), поскольку, добавляя 1-часовую дельту к настенным часам, вы фактически пропускаете временной интервал, в течение которого складка будет актуальной / Сделать разницу. В случае 2 / несуществующей даты и времени складка не имеет значения. Я не знаю, почему не реализована проверка, существует ли дата и время или нет.

FObersteiner 29.04.2024 14:27

Я предполагаю, что это связано с тем, что Python позволяет напрямую создавать несуществующие даты. Например. u = datetime(2022,3,27, 2, 30, tzinfo=zone) бегает без нареканий.

zap 29.04.2024 14:30

Другие вопросы по теме