Сегодня поговорим о дате-времени.
При этом я не хочу отдельно останавливаться на модуле 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.
-
Если по каким-то причинам вам приходится иметь дело
с относительным временем - сокращайте такие моменты до минимума,
возвращаясь к абсолютным временам сразу же как только возможно.
-
Помните: ввести абсолютные времена на разросшийся и уже массово
использующийся проект стоит чрезвычайно дорого.
Это касается не только переделки кода - зачастую требуется еще и
поправить все записи с временами в базе данных (при мысли о том,
что делать с резервными копиями мне становится нехорошо)
и, скажем, одномоментно обновить весь сопутствующий софт
у ваших клиентов.
Гораздо дешевле делать "правильно" с самого начала.
Предупрежден - значит вооружен!
Удачного вам плаванья и семи футов под килем!
Ссылки