Компания
1 088,83
рейтинг
9 ноября 2014 в 22:22

Разработка → «Eppur si muove!»* или Работаем с таймзонами в Python перевод

На нашей планете Земля, в одно и то же время, в разных географических точках планеты может быть разное время суток. Это следствие того, что наш мир — вращающийся геоид, а не плоский диск, а что наша Солнечная система имеет только одну звезду — Солнце. Ещё со школы всем известно о часовых поясах, и все мы встречались с их проявлениями в реальной жизни («Московское время – 15 часов, в Петропавловске-Камчатском – полночь», джетлаг при дальних перелётах, и т.д.). К несчастью, часовые пояса всего лишь частично основаны на физических особенностях нашего мира, и при компьютерных вычислениях приходится учитывать другие, порой неожиданные, нюансы.

* «И всё-таки она вертится!» — крылатая фраза, которую якобы произнёс Галилео Галилей, покидая процесс инквизиции после отречения от своего убеждения в том, что Земля вращается вокруг Солнца. В нашем случае, увы, это вращение приводит ко всем этим «замечательным» проблемам с часовыми поясами.

Что общего у этой статьи и Галилео? Да, в общем-то, ничего. Боюсь, что если бы наш мир был центром вселенной, нам всё равно пришлось бы иметь дело с таймзонами. Будем считать заголовок моей оплошностью, которую я уже не могу исправить (хотя я могу).

Что такое «Часовой пояс»?

Какой у вас часовой пояс? Если вы ответите «UTC+3» — это будет правильным ответом только на текущий момент времени, но в целом это заявление некорректно. Если вы посмотрите на базу данных часовых поясов, то увидите, к примеру, что Берлин и Вена, несмотря на смещение «UTC+1», имеют разные часовые пояса («Europe/Berlin» и «Europe/Vienna»). Почему так? Причина в том, что они имели разное летнее время (DST) в разные периоды истории. Даже если сегодня эти две страны и эти два города имеют одинаковые правила DST, сто лет назад это было не так. Например, и в Австрии и в Германии в разные периоды времени не было перехода на летнее время: в Австрии с 1920 года, а в Германии с 1918. Во время Второй мировой войны обе страны имели одинаковые правила DST (что не удивительно), однако после её окончания снова рассинхронизировались. Германия отменила переход на летнее время в 1949 и ввела его снова в 1979, Австрия же отменила DST в 1948 и ввела его снова в 1980. Самое же худшее состоит в том, что они даже не согласовали одинаковую дату перехода на летнее время.

И так происходит по всему миру. Для компьютерных вычислений, переход на летнее время — огромная проблема, ведь мы предполагаем, что время имеет непрерывный мотононный ход. С переходом на летнее время у нас каждый год есть час, который повторяется дважды, и есть час, который мы просто пропускаем. Если при записи в лог вы указываете локальное время, у вас можем нарушится порядок строк лога при сортировке.

Цитата из документации pytz:
Так, например, в таймзоне US/Eastern в 2002 году во время окончания действия DST, 27 октября время 01:30 наступило дважды, а во время начала действия DST, 7 апреля время 02:30 не наступило, т.к. в 02:00 часы перевели на час вперёд.

Но в таймзонах хранятся не только правила перехода на летнее время. Некоторые страны меняют часовые пояса, иногда даже без изменения DST. Так, например, в 1915 году Варшава перешла на Центральноевропейское время. В результате в полночь 5 августа 1915 года часы были переведены на 24 минуты назад (при этом в Варшаве действовало летнее время).
Вообще, с часовыми поясами творится ещё больший ад. Есть как минимум одна страна, таймзона которой была различна в течение дня из-за синхронизации времени 0:00 с временем восхода Солнца.

Где же здравый смысл?

Здравый смысл есть и он называется Всемирное координированное время (UTC). UTC — это таймзона без перехода на летнее время и без каких бы то ни было изменений в прошлом. Однако по причине того, что наша Земля — вращающийся геоид и в мире есть вещи, которые мы не можем контролировать, существует проблема корректировочных секунд (leap seconds). Будет ли UTC учитывать корректировочные секунды (которые нерегулярны и поэтому их достаточно проблематично учитывать при вычислениях), или не будет (тогда каждая таймзона будет иметь разницу в несколько секунд с UTC), — насколько мне известно, ещё не решено.

Несмотря на это, прямо сейчас UTC — самый безопасный вариант. Из UTC вы можете сконвертировать время в локальное для любого часового пояса. Обратное преобразование, учитывая вышесказанное, невозможно.

Итак, вот главное практическое правило, которое никогда вас не подведёт:
Всегда храните и работайте со временем в UTC. Если вам нужно сохранить оригинальные данные — пишите их отдельно. Никогда не храните локальное время и таймзону!

В чём проблема?

Вообще, на этом статья должна была бы закончиться. Но к несчастью, есть пара вещей, которые необходимо держать в памяти, когда вы программируете на Python. Это наследие архитектурных решений тех давних времён, когда никто не думал о практическом применении языка. Мотивация имела значение, здравый смысл — нет.

В один прекрасный день были приняты следующие решения об архитеутуре модуля datetime стандартной библиотеки Python:
  1. Модуль datetime не должен хранить информацию о таймзонах, потому что таймзоны меняются слишком часто.
  2. С другой стороны, модуль datetime должен давать возможность добавлять в себя информацию о таймзоне (tzinfo).
  3. В модуле datetime должны быть реализованы следующие объекты: date, time, date+time, timedelta.

К несчастью, что-то пошло не так. Основная проблема заключается в том, что объект datetime, в который была добавлена информация о таймзоне (tzinfo), не будет взаимодействовать с объектом datetime без таймзоны:
>>> import pytz, datetime
>>> a = datetime.datetime.utcnow()
>>> b = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
>>> a < b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

Если закрыть глаза на тот ужасный API, с помощью которого вам приходится добавлять информация о таймзоне к объекту datetime, всё равно остаются проблемы. Когда вы работаете с объектами datetime в Питоне, вам рано или поздно придётся добавлять или удалять tzinfo во всех местах вашей программы.

Другая проблема состоит в том, что у вас есть два способа создать объект datetime с текущим временем в Python:
>>> datetime.datetime.utcnow()
datetime.datetime(2011, 7, 15, 8, 30, 55, 375010)
>>> datetime.datetime.now()
datetime.datetime(2011, 7, 15, 10, 30, 57, 70767)

Один возвращает время в UTC, другой — локальное время. Однако объект datetime не скажет вам, что такое «локальное время» (потому что он не имеет информации о таймзоне, по крайней мере до версии Python 3.3), и нет никакого способа узнать, который из этих объектов хранит время в UTC.

Если вы конвертируете UNIX timestamp в объект datetime, вам так же следует быть осторожным при использовании метода datetime.datetime.utcfromtimestamp, потому что он принимает timestamp в локальном времени.

Библиотека datetime так же предоставляет объекты date и time, в которые абсолютно бесполезно добавлять tzinfo. Объект time не может быть переведён в другую таймзону, поскольку для этого нужно знать дату. Объект date вообще имеет смысл только для локальной таймзоны, потому что «сегодня» для меня может быть «вчера» или «завтра» для вас — скажем спасибо чудесному миру часовых поясов.

Так каковы рекомендации специалистов?

Теперь мы знаем, кто виноват. Но что делать? Если мы проигнорируем теоретические проблемы, проявляющиеся только в случае работы с историческими датами в прошлом, то вот вам ряд рекомендаций. На тот случай, если вам приходится работать с историческими датами, есть альтернативный модуль mxDateTime, достаточно качественно спроектированный и даже поддерживающий различные календари (Григорианский и Юлианский).

Используйте UTC внутри программы


Если вам нужно получить текущее время, всегда используйте datetime.datetime.utcnow(). Если вы получаете локальное время от пользователя, всегда тут же преобразовывайте его в UTC. Если однозначного преобразования сделать не получается — сообщайте об этом пользователю, не пытайтесь угадать его время вслепую. Во время перехода на летнее время и обратно, мой iPhone несколько раз не смог правильно перевести время. Я же знаю, когда это нужно сделать, поскольку мне приходится переводить стрелочные часы.

Никогда не используете время с часовым поясом


Это может показаться вам хорошей идеей — всегда добавлять информацию о часовом поясе к объектам datetime, но на самом деле гораздо лучшая идея — не делать этого. Хорошим решением будет использование объекта datetime без tzinfo и с временем по UTC. Учитывайте тот факт, что вы не можете сравнивать время с таймзоной с временем без неё, так же, как не можете смешивать bytes и unicode в Python 3. Используете этот недостаток API в своих целях.
  1. Внутри программы всегда используйте объекты datetime без tzinfo с временем по UTC.
  2. Когда вы взаимодействуете с пользователем, всегда конвертируйте его локальное время UTC и обратно.

Почему вам не нужно добавлять tzinfo в объект datetime? Во-первых, потому, что подавляющая часть библиотек ожидает, что tzinfo будет равно None. Во-вторых, это ужасная идея всегда работать с tzinfo, учитывая кривое API работы с ним. В библиотеке pytz есть альтернативные функции для конвертирования таймзон, потому что реализованное в стандартной библиотеке API для преобразования tzinfo недостаточно гибкое, чтобы работать с большинством реальных таймзон. Если мы не будем использовать объекты tzinfo, есть шанс, что в будущем всё изменится к лучшему.

Другая причина не использовать время с таймзоной заключается в том, что объект tzinfo очень специфичен и сильно зависит от своей реализации. Не существует стандартного способа передавать информацию о таймзоне (за исключением, пожалуй, таймзоны UTC) в другие языки, по HTTP и т.д. К тому же объекты datetime с информацией о таймзоне, зачастую, становятся слишком огромными при сериализации с помощью модуля pickle, или их даже невозможно бывает сериализовать (это зависит от реализации объекта tzinfo).

Преобразования для форматирования


Если вам нужно показать время в таймзоне пользователя, возьмите объект datetime с временем по UTC, добавьте в него таймзону UTC, преобразуйте время в локальное время пользователя и отформатируйте его. Не используйте преобразование таймзоны методами tzinfo, ибо они работают некорректно, используйте pytz. Потом переведите время в «наивное» путём отбрасывания смещения таймзоны из получившегося объекта datetime, который вы создали для форматирования и продолжайте жить счастливо.

Перевёл Dreadatour, текст читал %username%.


Бонус от переводчика для тех, кто дочитал до конца:


Шикарное видео от Tom Scott про таймзоны:


А в следующей своей статье я напишу, где автор неправ, почему он ошибается и как же всё-таки нужно делать правильно.
Автор: @Dreadatour Armin Ronacher

Комментарии (11)

  • 0
    Время два ночи, не до конца прочитал статью, комментарий был лишний. Надо было прочитать последний абзац.
  • +3
    Есть как минимум одна страна, таймзона которой была различна в течение дня из-за синхронизации времени 0:00 с временем восхода Солнца.


    а что за страна такая, и где про это почитать можно?
    • +1
      Я не знаю, что конкретно имел в виду автор, но, по всей видимости, он говорил о «Riyadh Solar Time». В Саудовской Аравии с 1987 по 1989 годы пытались использовать эту таймзону, но потом отказались.

      Вот цитата из рассылки о pytz (оригинал тут: http://mm.icann.org/pipermail/tz/2004-June/012495.html):
      536 of the Olsen timezones are supported. The missing few are for
      Riyadh Solar Time in 1987, 1988 and 1989. As Saudi Arabia gave up
      trying to cope with their timezone definition, I see no reason
      to complicate my code further to cope with them. (I understand
      the intention was to set sunset to 0:00 local time, the start of the
      Islamic day. In the best case caused the DST offset to change daily
      and worst case caused the DST offset to change each instant depending
      on how you interpreted the ruling.)

      Вот первая ссылка с правилами перехода, которую я нашёл (не ручаюсь за её достоверность): gist.github.com/NZKoz/5259788

      И ещё интересная ссылка с чуть более подробным описанием: blogs.kde.org/2005/11/25/timezones-and-experiment
      • +1
        кажется у меня настолько непечатных выражений в словарном запасе не водится, чтоб это описать. хотя идея забавная, да.
        • 0
          Это ещё цветочки, это уже в прошлом и, слава Богу, не используется. Вечером опубликую свою статью на эту тему, которую я написал, чтобы на примерах из нашей жизни показать, что с таймзонами всё ещё хуже, чем кажется.
  • +1
    Воистину инфосфера существует. Только вчера вечером курочил стандартные часы-календарь Cinnamon 2.4, что бы добавить туда вывод времени для разных часовых поясов (перетаскивал постепенно код из аплета World Clock Calendar, который отказался запускаться) и потребовалось для автодополнения часовых поясов что-то применить в скрипте на питоне, который был в WCC.

    Захотелось что бы часовой пояс подбирался и по короткому имени, типа VLAT, этого, кстати, в pytz не нашёл (пока костыльно сделал), как и названий зон типа RTZ9 (хотя я этого сочетания и в tzdata не вижу в последних)

    PS аплет, кому надо, скоро выложу.
    • 0
      Можно чуть поподробнее, зачем нужно подбирать пояс по короткому имени? Мне правда интересно.

      Мы в календаре столько настрадались со всеми этими названиями и сокращениями таймзон. Так, например, MS Outlook в разное время, в разных версиях и в разных локализациях ОС выдаёт таймзоны вида:
      — (GMT+03.00) Moscow / St. Petersburg / Volgograd
      — (GMT+04.00) Moscow / St. Petersburg / Volgograd
      — (UTC+04.00) Moscow / St. Petersburg / Volgograd
      — (UTC+04:00) Волгоград, Москва, Санкт-Петербург
      — Волгоград, Москва, Санкт-Петербург
      — Moscow, St. Petersburg, Volgograd
      — Russian Standard Time
      — Russian
      И ещё огромную кучу других вариантов. Поди потом найди соответствие нужному часовому поясу и разберись, что же этот аутлук имел в виду. А если всмомнить ещё про проблему с переводом часов в Windows — вообще за голову хватаешься.
      • 0
        Это чисто что бы выбрать в списке автодополнения, подставляться и использоваться будет стандартная зона. Хотя вот как догадаться, что для Оттавы нужно выбрать America/Nassau я не знаю. Но хотя бы, что бы вывелось:
        America/Nassau, EST/EDT

        и видя, что есть переход между летним (EDT) и зимним (EST) временем, подобрать что-то подходящее.

        В любом случая, я не собираюсь выходить за рамки именования зон, принятых в tzdata. Плюс, всё же, целевая аудитория — это пользователи Cinnamon, а его работа в Windows явно не основная цель разработчиков.
  • +3
    «Вот так заходишь в профиль к человеку, подкинуть кармы, а ты это уже сделал когда-то» ©
    Спасибо за перевод!
  • 0
    У всех этих проблем есть красивое и элегантное решение.

    Не благодарите.
    • 0
      Arrow — всего лишь инструмент. Красивый, элегантный, да, и все, кто в теме, давно уже его используют где нужно.

      Но любой инструмент важно применять по назначению. Если вы будете красивым и элегантным микроскопом копать яму, то… Ну удачи, что :)

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

Самое читаемое Разработка