Как-то я изучал release notes новой версии Python 3.12, и в разделе о депрекациях моё внимание привлекла следующая фраза:
utcnow()
иutcfromtimestamp()
изdatetime.datetime
устарели и будут удалены в будущей версии.
Если вы следили за моими туториалами по веб-разработке, то видели, что я часто использую utcnow()
; очевидно, мне придётся переучиваться и использовать альтернативу, готовясь к неизбежному удалению этой функции (вероятно, это произойдёт спустя несколько лет, так что причин для паники нет!).
В этой краткой статье я подробнее расскажу о том, почему эти функции попали под нож и чем их можно заменить.
Что не так с utcnow() и utcfromtimestamp()?
Обнаруженная мейнтейнерами Python проблема проистекает из того факта, что эти функции возвращают «наивные» временные объекты. Наивный объект datetime
не содержит часового пояса, то есть он может использоваться в контексте, когда часовой пояс неважен или известен заранее. Это противоположно «осведомлённым» объектам datetime
, к которым явным образом прикреплён часовой пояс.
На мой взгляд, имена этих функций сбивают с толку. Следует ожидать, что функция с именем utcnow()
возвращает время UTC, как и следует из имени. Я бы сделал более очевидным то, что эти функции работают с наивным временем, например, назвав их naive_utcnow()
и naive_utcfromtimestamp()
.
Но проблема здесь не в их именах. Конкретная проблема заключается в том, что некоторые функции даты и времени в Python принимают наивные метки времени и предполагают, что они описывают локальное время согласно часовому поясу, настроенному на компьютере, где исполняется код. На GitHub есть issue, датированная 2019 годом, объясняющая это и снабжённая следующим примером:
>>> from datetime import datetime
>>> dt = datetime.utcfromtimestamp(0)
>>> dt
datetime.datetime(1970, 1, 1, 0, 0)
>>> dt.timestamp()
18000
Этот пример исполнялся на компьютере с североамериканским восточным временем (Eastern Standard Time, EST). Сначала dt
присваивается наивный datetime
, преобразованный из «нулевого» времени или UNIX-времени, то есть полуночи 1 января 1970 года.
Когда этот объект преобразуется обратно в метку времени, метод dt.timestamp()
обнаруживает, что он не имеет часового пояса, который можно использовать в преобразовании, поэтому использует собственный часовой пояс компьютера, которым в этом примере был EST (следует учесть, что часовой пояс EST на 5 часов, или на 18000 секунд отстаёт от UTC). То есть у нас есть метка времени UNIX, изначально бывшая полуночью 1 января 1970 года, а после преобразования в datetime
и обратно оказывающаяся пятью часами утра.
Автор issue по ссылке выше предполагает, что эта неоднозначность не существовала в Python 2 и по этой причине долгое время не была проблемой, но теперь стала, и её нужно решать. Это показалось мне странным, так что я решил проверить, и действительно, метод timestamp()
, возвращающий некорректное UNIX-время в примере, был введён Python 3.3 и ничего подобного во времена Python 2, похоже, не существовало.
То есть, по сути, в какой-то момент мейнтейнеры добавили метод datetime.timestamp()
(а возможно, и другие), принимающий осведомлённые и наивные временные объекты, и это было ошибкой, потому что этим методам нужен часовой пояс.
Эти методы нужно было спроектировать так, чтобы они выдавали ошибку при передаче им наивного объекта datetime
, но по какой-то странной причине разработчики решили, что если часовой пояс не указан, то следует использовать пояс из системы. На самом деле, это баг, но вместо того, чтобы чинить поломанные реализации этих методов, мейнтейнеры, выполнив депрекацию двух основных функций, которые генерируют наивные объекты, теперь пытаются заставить людей переходить на осведомлённые временные объекты. Они считают так: поскольку некоторые функции предполагают, что наивные метки времени обозначают локальное время, то всем наивным меткам, не относящимся к локальному времени, необходимо препятствовать.
Возможно, я что-то упускаю, но не совсем понимаю эту логику.
Нужны ли нам вообще наивные временные объекты?
Мне очевидно, что мейнтейнеры Python, принявшие решение о депрекации, видят проблему в наивных временных объектах и используют эту предполагаемую проблему как повод их удалить.
Так почему вообще кому-то захочется работать с наивными временными объектами?
Приложение может быть спроектировано таким образом, что все даты и всё время находятся в одном часовом поясе, известном заранее. В таком случае нет необходимости в том, чтобы отдельные экзепляры datetime
хранили собственные часовые пояса, потому что для этого требуется больше памяти и вычислительных ресурсов, а никакой выгоды это не даёт: все эти часовые пояса будут одинаковыми и никогда не придётся выполнять вычисления с часовыми поясами или их преобразования.
На самом деле, такое очень часто встречается в веб-приложениях и других типах сетевых серверов, сконфигурированных со временем UTC и нормализующих все попадающие в систему даты и всё время в этот часовой пояс. Кроме того, лучше всего хранить в базах данных наивные временные объекты в UTC. Например, по умолчанию тип DateTime в SQLAlchemy задаёт наивный объект datetime
. Это настолько распространённый паттерн в базах данных, что SQLAlchemy предоставляет рецепт для приложений, использующих осведомлённые объекты datetime
, по преобразованию их в наивные объекты и обратно на лету при загрузке и считывании из базы данных.
Поэтому да, я считаю, что наивные объекты datetime
будут продолжать использовать, несмотря на эти депрекации.
Как обновить свой код
Хотя эти депрекации меня разочаровывают, важно помнить, что до удаления функций может пройти несколько лет. Проблема в том, что после перехода на Python 3.12 или более новую версию вы начнёте видеть в консоли и логах сообщения о депрекации, которые могут раздражать. Вот пример того, что вы будете видеть:
$ python
Python 3.12.0 (main, Oct 5 2023, 10:46:39) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import datetime
>>> datetime.utcnow()
<stdin>:1: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime.datetime(2023, 11, 18, 11, 22, 54, 263206)
Я использую Python 3.12 только в небольшой части проектов, но уже утомился от этих сообщений. Так что давайте посмотрим, как же можно заменить эти две функции.
Мейнтейнеры Python рекомендуют переходить на осведомлённые объекты datetime
. В предупреждении о депрекации есть подсказка о том, что, по их мнению, вы должны использовать, а уведомления о депрекации в документации ещё более конкретны. Вот, что написано в уведомлении о функции utcnow()
:
Депрекация с версии 3.12: вместо неё используйте
datetime.now()
сUTC
.
Ниже есть уведомление о utcfromtimestamp()
:
Депрекация с версии 3.12: вместо неё используйте
datetime.fromtimestamp()
сUTC
.
Это даёт нам представление о том, что нужно сделать. Вот мои собственные версии этих функций с дополнительной опцией для выбора между осведомлённой или наивной реализациями:
from datetime import datetime, timezone
def aware_utcnow():
return datetime.now(timezone.utc)
def aware_utcfromtimestamp(timestamp):
return datetime.fromtimestamp(timestamp, timezone.utc)
def naive_utcnow():
return aware_utcnow().replace(tzinfo=None)
def naive_utcfromtimestamp(timestamp):
return aware_utcfromtimestamp(timestamp).replace(tzinfo=None)
print(aware_utcnow())
print(aware_utcfromtimestamp(0))
print(naive_utcnow())
print(naive_utcfromtimestamp(0))
Стоит отметить, что если вы работаете с Python 3.11 или старше, то можно заменить datetime.timezone.utc
на более короткую datetime.UTC
.
При выполнении этого скрипта я получил следующие результаты:
2023-11-18 11:36:35.137639+00:00
1970-01-01 00:00:00+00:00
2023-11-18 11:36:35.137672
1970-01-01 00:00:00
По +00:00
понятно, что в первой и второй строке указаны осведомлённые экземпляры datetime
, обозначающему часовой пояс 00:00, или UTC. В третьей и четвёртой строках указаны абстрактные метки времени без часового пояса, полностью совместимые с метками, которые возвращаются метками, подвергшимися депрекации.
В этих реализациях мне нравится то, что они позволяют выбирать, работать ли с часовыми поясами или без них, что устраняет все неоднозначности. Как говорится в старом изречении, явное лучше неявного.
Комментарии (5)
Dominux
20.11.2023 14:13Если вы следили за моими туториалами по веб-разработке, то видели, что я часто использую utcnow(); очевидно, мне придётся переучиваться и использовать альтернативу
Всегда интересно наблюдать, как интерн создаёт туториалы. Жаль, конечно, что это лишь перевод
baldr
20.11.2023 14:13Не совсем понятна ваша коннотация к слову "интерн", но выглядит как что-то негативное. Однако, на Мигеля Гринберга бочку катить не надо, пожалуйста. Вы список его репозиториев видели? Лично я использую его
python-socketio
постоянно.
S-trace
20.11.2023 14:13А в чём собственно проблема была заставить эти "наивные" функции попросту отдавать время с поясом UTC?
Торвальдса на них нет, тот быстро бы объяснил им, что нельзя ломать существующие программы.. Хотя, питон и так необратимо сломали на грани 2-3 версий (за что от меня им отдельный луч ненависти, по сей день порой попадаются скрипты для второй версии, и это боль).
andreymal
20.11.2023 14:13+1В том, что слепая замена naive datetime на aware datetime скорее всего сломает программы ещё сильнее (и, что самое страшное, может сломать незаметно, а тут хотя бы сразу с очевидной ошибкой грохнется)
winmasta
Еще можно ZoneInfo https://docs.python.org/3/library/zoneinfo.html использовать для работы с таймзонами