На этом шаге мы рассмотрим особенности работы с датой и временем.
Материал этого шага взят:
В обычной жизни мы оперируем некоторым локальным временем, которое действует там, где мы живём, однако, в компьютерных системах с ним работать сложно и опасно - из-за перевода часов (летнее время, госдума и т.п.) оно неравномерно и неоднозначно. Поэтому требуется некоторое универсальное время, которое обладает равномерностью и однозначностью, одно значение которого отображает один и тот же момент времени в любой точке Земли - единая точка отсчёта, её роль исполняет UTC - всемирное координированное время. А ещё нам потребуются часовые пояса (часовые зоны в современной терминологии), чтобы конвертировать локальное время в универсальное и наоборот.
А что же такое вообще часовой пояс?
Во-первых, это смещение от UTC. То есть на какое количество часов и минут наше локальное время отличается от UTC. Заметьте, что это не обязательно должно быть целое число часов. Так, Индия, Непал, Иран, Новая Зеландия, части Канады и Австралии и многие другие живут с отличием от UTC в X часов 30 минут или X часов 45 минут. Более того, в некоторые моменты на Земле действуют аж три даты - вчера, сегодня и завтра, так как разница между крайними часовыми поясами - 26 часов.
Во-вторых, это правила перехода на летнее время. Среди стран, имеющих часовые пояса с одинаковым смещением, некоторые не переходят на летнее время совсем, некоторые переходят в одних числах, другие - в других. Некоторые летом, некоторые зимой (да, у нас есть южное полушарие). Некоторые страны (в том числе Россия) переходили на летнее время раньше, но мудро отказались от этой идеи. И для правильного отображения даты и времени в прошлом это всё нужно учитывать. Важно помнить, что при переходе на летнее время меняется именно смещение (было в Москве раньше +3 часа зимой, становилось +4 летом).
В компьютерах информация для работы с этим безумием хранится в соответствующих базах данных, все хорошие библиотеки для работы со временем умеют все эти жуткие особенности учитывать.
В Windows вроде бы используется какая-то своя база, а практически во всём опенсурсном мире стандарт де-факто - это база данных часовых поясов IANA Time Zone Database, более известная как tzdata. В ней хранится история всех часовых поясов с начала эпохи Unix, то есть с 1-го января 1970-го года: какие часовые пояса когда появлялись, какие когда исчезали (и в какие они вливались), где и когда переходили на летнее время, как по нему жили и когда его отменяли. Каждый часовой пояс обозначается как Регион/Место, например, московский часовой пояс называется Europe/Moscow. Tzdata используется в GNU/Linux, Java, Ruby (гем tzinfo), PostgreSQL, MySQL и ещё много где.
Давайте посмотрим на элементарный пример:
>>> from datetime import * >>> dt = datetime.now()
Правильно ли так писать? Ответ будет довольно неожиданным: когда как...
Дело в том, что в программах, как бы это парадоксально не звучало, одновременно существуют два типа времени, которые логически не перекрываются. Назовем их относительным и абсолютным временем.
Относительное время. Этот тип времени никогда не пересекает границ программы, не сохраняется в базе данных и не передается по сети.
Используется для разных целей: измерения временных интервалов, общения с пользователем и так далее.
У относительного времени (naive в английской терминологии) нет информации о временной зоне (timezone). Наш простейший пример создавал именно такое время.
Большинство используемых в небрежно составленной программе времен - относительные. И это далеко не всегда корректно.
Абсолютное время. В противовес относительному обладает временной зоной, которая вдобавок не пустая.
В английском для таких времен применяют термин aware. Перевод на русский звучит как осведомленный, но в некоторых источниках переводится как абсолютное время.
Временные зоны - это смещения относительно времени по Гринвичу. Объекты datetime и time могут принимать необязательный парамер по имени tzinfo. Этот параметр должен быть или None или экземпляром класса, унаследованного от базового абстрактного класса tzinfo.
Отметим, что стандартная библиотека Python не поставляется с реализациями tzinfo. Документация для класса tzinfo содержит полезные примеры. Обратите внимание на большой блок кода в конце раздела.
Несколько советов по работе со временем.
Когда пишем время в базу данных или пересылаем с одной машины на другую - во избежание проблем следует пользоваться абсолютным временем, а еще лучше приводить время к Гринвичу (так называемому UTC).
Дело в том, что у разных машин могут быть разные временные зоны, а Киевское время, например, отличается от Московского на один час. В результате одно и то же, казалось бы, время будет означать в Москве не совсем то, что оно означает в Киеве. В Москве может быть уже "завтра", когда в Киеве еще "сегодня". Если же во всех внешних коммуникациях используется UTC (которое не имеет летнего времени) - всё получается однозначно.
По умолчанию объект datetime.datetime (всё сказанное применимо и к datetime.time) создается как относительное время, то есть без временной зоны.
Более того, существует два метода получить текущее время: datetime.now() и datetime.utcnow(). Полученные значения, конечно, различаются на действующую сейчас временную разницу. При этом вы не можете программно понять, где время в UTC, а где - локальное.
Базы данных вносят дополнительные оттенки. Некоторые могут хранить времена в абсолютном формате, другие - нет. Впрочем, это не важно - даже если конкретная база данных поддерживает абсолютное время и библиотека для работы с этой базой умеет понимать временные зоны - используемая вами объектно-реляционная надстройка (ORM) скорее всего это ценное умение игнорирует.
Поэтому единственный надежный способ работы с базами данных - использовать относительное время, явно преобразуя его в UTC.
На базах данных странности не кончаются.
Дело в том, что только кажется: в Python есть лишь один datetime.datetime. Хотя технически все объекты класса datetime.datetime используют этот единственный тип, на самом деле концептуально абсолютные времена (с указанной зоной) никак не совместимы с относительными.
Т.е. вы не можете сравнить абсолютное и относительное время, вычесть из одного другое и так далее - на любую подобную попытку получите TypeError.
И, как уже было отмечено, стандартная библиотека Python не имеет временных зон. Она содержит абстрактный класс tzinfo - и ни одной его конкретной реализации. Остается вопрос, где эти временные зоны брать.
Существует прекрасная библиотека dateutil. Она много чего умеет, неплохо расширяя стандартную datetime.
Что касается временных зон:
>>> from dateutil.tz import tzutc, tzlocal >>> tzutc = tzutc() >>> tzlocal = tzlocal()
Если вам требуется получить зону по имени - используйте библиотеку pytz.
>>> import pytz >>> tzmoscow = pytz.timezone("Europe/Moscow") >>> print moscow Europe/Moscow
Классы временных зон для 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-библиотека не поддерживает временные зоны.
На следующем шаге мы рассмотрим класс datetime.