Шаг 105.
Основы языка Python. Работа с датой и временем. Модуль datetime. Манипуляции датой и временем. Временные зоны

    На этом шаге мы рассмотрим особенности работы с датой и временем.

    Материал этого шага взят:

    В обычной жизни мы оперируем некоторым локальным временем, которое действует там, где мы живём, однако, в компьютерных системах с ним работать сложно и опасно - из-за перевода часов (летнее время, госдума и т.п.) оно неравномерно и неоднозначно. Поэтому требуется некоторое универсальное время, которое обладает равномерностью и однозначностью, одно значение которого отображает один и тот же момент времени в любой точке Земли - единая точка отсчёта, её роль исполняет 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.




Предыдущий шаг Содержание Следующий шаг