Разработка, HowTo

Python: Чем плох datetime.replace?

часыПоговорим сегодня про даты и часовые пояса. А именно о том, почему не стоит использовать datetime.replace совместно с таймзонами из pytz если вы не уверены (вообще не стоит).

Конечно все с оговорками. Иногда так надо. Но все равно не стоит так делать.

Вводные:

>>> from datetime import datetime
>>> import pytz
>>> dt = datetime.strptime('2019-09-01 12:00:00', '%Y-%m-%d %H:%M:%S')
>>> tzdata = pytz.timezone('Europe/Moscow')
>>> dt_withreplace = dt.replace(tzinfo=tzdata)
>>> dt_withlocalize = tzdata.localize(dt)

Переводем обе даты в UTC.

>>> dt_withlocalize.astimezone(pytz.utc)
datetime.datetime(2019, 9, 1, 9, 0, tzinfo=<UTC>)
>>> dt_withreplace.astimezone(pytz.utc)
datetime.datetime(2019, 9, 1, 9, 30, tzinfo=<UTC>)

Спорим, что вы ожидали вовсе не этого? Откуда взялось отставание в 30 минут при использовании replace?

Рассмотрим содержимое двух дат с таймзонами.

>>> dt_withreplace
datetime.datetime(2019, 9, 1, 12, 0, tzinfo=<DstTzInfo 'Europe/Moscow' LMT+2:30:00 STD>)
>>> dt_withlocalize
datetime.datetime(2019, 9, 1, 12, 0, tzinfo=<DstTzInfo 'Europe/Moscow' MSK+3:00:00 STD>)

Ок. MSK — это хорошо, но что такое LMT?

LMT (local mean time) — местное среднее время. Если простыми словами, то это время формировалось для некоторого мередиана на основании солнечного времени и солнечных часов.

Если заглянуть в вики, то можно прочесть, что этот тип учета времени использовался до введения часовых поясов. Так почему современный питон применил его к дате?

Нам потребуется заглянуть внутрь функции localize и replace.

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

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

Вот только есть одно но. Новый tzinfo — имеет класс DstTzInfo.

Он имплементирует методы объекта datetime.tzinfo. И если мы чуть-чуть прогуляемся по коду, то увидим, что вызывается utcoffset.

Посмотрим, что он нам вернет в tzdata.

>>> tzdata.utcoffset(dt)
datetime.timedelta(seconds=10800)
>>> tzdata.utcoffset(dt_withreplace)
datetime.timedelta(seconds=9000)
>>> tzdata.utcoffset(dt_withlocalize)
Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python3.7/site-packages/pytz/tzinfo.py", line 422, in utcoffset
    dt = self.localize(dt, is_dst)
  File "/usr/lib/python3.7/site-packages/pytz/tzinfo.py", line 318, in localize
    raise ValueError('Not naive datetime (tzinfo is already set)')
ValueError: Not naive datetime (tzinfo is already set)

Если мы еще чуть больше покопаемся в коде pytz.DstTzInfo, то увидим, что он является прокси, который реализует все методы оригинального datetime.tzinfo, но при этом содержит в себе определения таймзон за разные периоды времени.

Когда мы пытаемся использовать его через подстановку в конструктор datetime, то ничего хорошего не будет. Он просто начнет возвращаеть первое определение часового пояса в своем внутреннем списке (_utc_transition_times, _tzinfos, _transition_info). На нашу беду первым в списке стоит LMT.

Чтобы такого казуса не произошло следует использовать метод pytz.DstTzInfo.localize. Именно в нем происходит вся магия выбора пояса в зависимости от даты.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s