воскресенье, 27 февраля 2011 г.

Питон: времена, даты и временные зоны

Сегодня поговорим о дате-времени.

При этом я не хочу отдельно останавливаться на модуле time - слишком он низкоуровневый.

Эта статья будет почти исключительно посвящена модулю datetime, предоставляющему довольно красивый и понятный интерфейс.

Давайте посмотрим на элементарный пример:

>>> from datetime import *
>>> dt = datetime.now()

Что может быть проще?

Правильно ли так писать? Ответ будет довольно неожиданным: когда как...

Дело в том, что в программах, как бы это парадоксально не звучало, одновременно существуют два типа времени, которые логически не перекрываются.

Назовем их относительным и абсолютным временем.

Относительное время

Этот тип времени никогда не пересекает границ программы, не сохраняется в базе данных и не передается по сети.

Используется для разных целей: измерения временных интервалов, общения с пользователем и так далее.

У относительного времени (naive в английской терминологии) нет информации о временной зоне (timezone). Наш простейший пример создавал именно такое время.

Большинство используемых в небрежно составленной программе времен - относительные. И это далеко не всегда корректно.

Абсолютное время

В противовес относительному обладает временной зоной, которая вдобавок не пустая.

В английском для таких времен применяют термин aware. Перевод на русский звучит как осведомленный, что мне решительно не нравится. Пусть так и остается абсолютным временем.

Временные зоны

Позвольте немного подробней рассказать о временных зонах.

Это наши привычные смещения относительно времени по Гринвичу. Например, в моём Киеве локальное время отличается на +2 часа (летом +3).

Объекты datetime и time могут принимать необязательный парамер по имени tzinfo.

Этот параметр должен быть или None или экземпляром класса, унаследованного от базового абстрактного класса tzinfo.

Я сейчас не хочу подробно останавливаться на том, как правильно унаследоваться от tzinfo и что нужно переопределить. Достаточно знать, что объекты временных зон существуют.

Теория работы с абсолютным временем

Когда пишем время в базу данных или пересылаем с одной машины на другую - во избежание проблем следует пользоваться абсолютным временем, а еще лучше приводить время к Гринвичу (так называемому UTC).

Дело в том, что у разных машин могут быть разные временные зоны, а Киевское время, например, отличается от Московского на один час. В результате одно и то же, казалось бы, время будет означать в Москве не совсем то, что оно означает в Киеве. В Москве может быть уже "завтра", когда у меня еще "сегодня".

Чтобы окончательно всё запутать, существует летнее время. Дата перехода на него отличается от страны к стране. Если в Украине летнее время начинается в последнее воскресенье марта, то в Бразилии, насколько я помню, оно заканчивается в последнее воскресенье февраля (при условии что эта дата не совпадает с их праздником Карнавала).

В общем, без справочника не разберешься, и разница между локальным временем и UTC "на сегодня" завтра может измениться в ту или иную сторону.

Если же во всех внешних коммуникациях используется UTC (которое не имеет летнего времени, между прочим) - всё получается однозначно.

К сожалению, об абсолютных временах и "правиле UTC" очень часто забывают при разработке.

В результате к проблеме возвращаются, когда уже есть, например, довольно большая база данных и немаленькие программы, работающие с ней.

В таких условиях осуществить миграцию на "правильный" способ уже практически нереально.

Особенности реализации относительного и абсолютного времени в Питоне

В целях окончательного усложнения жизни программиста Питон сам по себе добавляет несколько интересных особенностей.

По умолчанию объект datetime.datetime (всё сказанное применимо и к datetime.time) создается как относительное время, то есть без временной зоны.

Более того, существует два метода получить текущее время: datetime.now() и datetime.utcnow(). Полученные значения, конечно, различаются на действующую сейчас временную разницу. При этом вы не можете программно понять, где время в UTC а где - локальное.

Базы данных вносят дополнительные оттенки. Некоторые могут хранить времена в абсолютном формате, другие - нет. Впрочем, это не важно - даже если конкретная база данных поддерживает абсолютное время и библиотека для работы с этой базой умеет понимать временные зоны - используемая вами объектно-реляционная надстройка (ORM) скорее всего это ценное умение игнорирует.

Поэтому единственный надежный способ работы с базами данных - использовать относительное время, явно преобразуя его в UTC.

На базах данных странности не кончаются.

Дело в том, что только кажется: в Питоне есть лишь один datetime.datetime. Хотя технически все объекты класса datetime.datetime используют этот единственный тип, на самом деле концептуально абсолютные времена (с указанной зоной) никак не совместимы с относительными.

Т.е. вы не можете сравнить абсолютное и относительное время, вычесть из одного другое и так далее - на любую подобную попытку получите TypeError.

И, чтобы окончательно свести с ума; забить, так сказать, последний гвоздь в крышку гроба - стандартная питоновская библиотека не имеет временных зон.

Она содержит абстрактный класс tzinfo - и ни одной его конкретной реализации.

Лишь в Python 3.2 появилась зона datetime.utc. Локальной зоны всё ещё нет.

После всего перечисленного неудивительно, что повсеместно используется относительное время в его худшем варианте - локальное для конкретной машины, без информации о том, какая зона здесь установлена.

При передаче этого относительного времени на другую машину получаем неопределенность, чреватую неожиданностями. Ну, дальше вы поняли...

Абсолютные времена, pytz и dateutil

Для более или менее комфортной жизни в этом сумасшедшем доме я рекомендую следующее.

Работайте с временами в базе данных только как с относительными (никуда не денешься), но храните их исключительно в UTC.

При обработке сразу же добавляйте временную зону UTC в явном виде. Для ORM процесс можно автоматизировать, унаследовавшись от существующего в вашей ORM описания колонки (поля) DateTime и добавив в преобразователи явное приведение временных зон. То же самое относится и, например, к библиотекам GUI.

Везде в программе используйте только абсолютные времена, особенно если у этих времен именно абсолютный контекст.

Вообще-то говоря, к примеру, измерить время выполнения алгоритма можно и применяя относительное время.

Но работа исключительно с абсолютными временами вносит полезную строгость и дисциплину. Раз уж вам хоть где-то потребовались абсолютные времена - применяйте их везде без компромиссов.

Операции с абсолютными временами безопасны, даже если они относятся к разным временным зонам - Питон всё учитывает.

Остается вопрос, где эти временные зоны брать.

Существует прекрасная библиотека dateutil.

Она много чего умеет, неплохо расширяя стандартную datetime.

Что касается временных зон:

>>> from dateutil.tz import tzutc, tzlocal
>>> tzutc = tzutc()
>>> tzlocal = tzlocal()

Если вам требуется получить зону по имени - используйте библиотеку pytz.

>>> import pytz
>>> tzkiev = pytz.timezone('Europe/Kiev')
>>> print tzkiev
Europe/Kiev

Классы временных зон для dateutil и pytz немного отличаются списком доступных методов. Вы можете прочесть все детали в документации. Для этого изложения важно то, что они оба поддерживают интерфейс, требуемый для datetime.tzinfo.

Теперь минимальный набор операций.

Получение пресловутого текущего времени:

>>> now = datetime.now(tzlocal)
>>> print now
2011-02-25 11:52:59.887890+02:00

Преобразование в UTC:

>>> utc = now.astimezone(tzutc)
>>> print utc
2011-02-24 09:52:59.887890+00:00

Объект utc можно класть в базу данных или пересылать по сети - он преобразован с учетом смещения.

Отбросить временную зону (если, например, база данных не игнорирует эту информацию, а требует явного относительного времени):

>>> naive_utc = utc.replace(tzinfo=None)
>>> print naive_utc
2011-02-24 09:52:59.887890

Добавить временную зону к объекту, полученному из базы данных

>>> utc2 = naive_utc.replace(tzinfo=tzutc)
>>> print utc2
2011-02-24 09:52:59.887890+00:00

Преобразовать абсолютное UTC время в абсолютное локальное

>>> local_dt = utc.astimezone(tzlocal)
>>> print local_dt
2011-02-25 11:52:59.887890+02:00

Те же операции следует проводить и с GUI, переводя в локальное относительное время и обратно, если GUI библиотека не поддерживает временные зоны.

Итоги

  • Старайтесь работать с абсолютными временами.

  • Для общения с внешним миром предпочитайте UTC.

  • Если по каким-то причинам вам приходится иметь дело с относительным временем - сокращайте такие моменты до минимума, возвращаясь к абсолютным временам сразу же как только возможно.

  • Помните: ввести абсолютные времена на разросшийся и уже массово использующийся проект стоит чрезвычайно дорого.

    Это касается не только переделки кода - зачастую требуется еще и поправить все записи с временами в базе данных (при мысли о том, что делать с резервными копиями мне становится нехорошо) и, скажем, одномоментно обновить весь сопутствующий софт у ваших клиентов.

    Гораздо дешевле делать "правильно" с самого начала.

Предупрежден - значит вооружен!

Удачного вам плаванья и семи футов под килем!

Ссылки