«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 про таймзоны:


А в наступній своїй статті я напишу, де автор неправий, чому він помиляється і як же все-таки потрібно робити правильно.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.