Модуль zoneinfo. Поддержка актуальных часовых поясов в Python

Существует так называемая IANA Time Zone Database (tz database), которая содержит информацию обо всех часовых поясах, используемых в мире. Каждый пояс представлен в ней файлом, содержащем сведения о названии зоны, смещении от UTC, переходе на летнее время, исторических изменениях. Сбором данных и поддержкой актуальности этой базы занимается IANA (одна из организаций по стандартизации интернета).

Эта база данных у вас уже может быть установлена, так как используется многими операционными системами для своих нужд и периодически обновляется. Сам по себе модуль zoneinfo стандартной библиотеки языка Python не содержит информации о часовых поясах. Он предназначен для получения данных из tz database и их использования в коде Python.


Чтобы узнать локацию файлов часовых поясов, можно воспользоваться константой TZPATH модуля:

Если в системе не установлена tz database (скорее всего в Windows) или необходимо обеспечить кроссплатформенную совместимость программы, тогда следует установить собственный (для Python) пакет с часовыми поясами: tzdata.

Модуль zoneinfo содержит класс ZoneInfo, от которого создаются соответствующие python-экземпляры переданных в конструктор зон.

from zoneinfo import ZoneInfo
 
moscow = ZoneInfo('Europe/Moscow')
cuba = ZoneInfo('Cuba')
 
print(moscow)  # Europe/Moscow
print(cuba)  # Cuba
 
luna = ZoneInfo('Luna')  # ZoneInfoNotFoundError

Ключ, который передается в конструктор ZoneInfo, является именем файла в директории zoneinfo (там, где располагается база данных). Если файл зоны находится в подкаталоге, то ключ отражает относительный путь до этого файла. Если файл указанного пояса не найден, то выбрасывается исключение ZoneInfoNotFoundError.

Узнать имена всех зон можно не только просматривая каталог, также с помощью имеющейся в модуле функции available_timezones.

import zoneinfo
 
print(zoneinfo.available_timezones())

Функция возвращает множество из строк, представляющих собой имена зон.

Экземпляры ZoneInfo не предназначены для непосредственного использования. Обычно их передают в качестве аргумента параметру tzinfo конструктора datetime.

from datetime import datetime
from zoneinfo import ZoneInfo
 
a = datetime(2023, 5, 27, hour=3,
             tzinfo=ZoneInfo('Asia/Tokyo'))
print(a)
print(a.utcoffset(), a.tzname(), a.tzinfo)
print(a.tzinfo.__repr__())
 
print()
 
b = a.astimezone(ZoneInfo('Europe/Moscow'))
print(b)
print(b.utcoffset(), b.tzname(), b.tzinfo)
2023-05-27 03:00:00+09:00
9:00:00 JST Asia/Tokyo
zoneinfo.ZoneInfo(key='Asia/Tokyo')

2023-05-26 21:00:00+03:00
3:00:00 MSK Europe/Moscow

Технически объекты типа ZoneInfo можно передавать и в конструктор time, однако особого смысла в этом нет.

from datetime import *
from zoneinfo import ZoneInfo
 
moscow = ZoneInfo('Europe/Moscow')
 
a = time(20, 30, tzinfo=moscow)  # Время наивное, хотя tzinfo не равен None
b = time(20, 30, tzinfo=timezone(timedelta(hours=3)))
print(a)  # 20:30:00
print(b)  # 20:30:00+03:00
print(a.utcoffset(), '|', a.tzinfo)  # None | Europe/Moscow
print(b.utcoffset(), '|', b.tzinfo)  # 3:00:00 | UTC+03:00
print(type(a.tzinfo))  # <class 'zoneinfo.ZoneInfo'>
print(type(b.tzinfo))  # <class 'datetime.timezone'>
 
c = time(20, 30)
 
print(a > c)  # Сравнивнение работает: оба времени наивны
# print(a > b)  # Будет ошибка: нельзя так сравнивать осведомленное с наивным
 
# Экземпляр ZoneInfo передается из экземпляра time и
# для экземпляра datetime создается таймдельта, которую
# возвращает utcoffset()
d = datetime.combine(date.today(), a)  # Это осведомленное время
print(d)  # 2023-05-27 20:30:00+03:00
print(d.utcoffset())  # 3:00:00

Причина, по которой таймдельта не вычисляется для объектов типа time видимо связана с тем, что tz database, с которой работает zoneinfo, помимо прочего хранит информацию о переходе на летнее время и обратно. Поэтому, если нет даты, то нельзя точно указать смещение от UTC для конкретного часового пояса. Оно может отличаться для летнего и зимнего времени.

Модуль zoneinfo освобождает программиста от необходимости разработки кода, обслуживающего изменение смещения часового пояса в течение года.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
 
a = datetime(2023, 5, 27, hour=3,
             tzinfo=ZoneInfo('Europe/Berlin'))
print(a)
print('Летнее DST:', a.dst())
 
b = a + timedelta(days=190)
print(b)
print('Зимнее DST:', b.dst())
 
c = b + timedelta(days=200)
print(c)
print('Опять летнее:', c.dst())
2023-05-27 03:00:00+02:00
Летнее DST: 1:00:00
2023-12-03 03:00:00+01:00
Зимнее DST: 0:00:00
2024-06-20 03:00:00+02:00
Опять летнее: 1:00:00

Метод dst() экземпляра datetime возвращает объект типа timedelta, смысловое назначение которого показать, включает ли смещении от UTC поправку на летнее время. Например, если две даты имеют смещение в 2 часа, но одна из них летняя, а другая зимняя, то они могут представлять собой разные часовые пояса.

Следует отметить, что не во всех странах есть переход на летнее время.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
 
a = datetime(2023, 5, 27, 12, 45, 59,
             tzinfo=ZoneInfo('Europe/Moscow'))
print(a)
print('Летнее DST:', a.dst())
 
b = a + timedelta(days=190)
print(b)
print('Зимнее DST:', b.dst())
2023-05-27 12:45:59+03:00
Летнее DST: 0:00:00
2023-12-03 12:45:59+03:00
Зимнее DST: 0:00:00

Также обратим внимание, что когда происходит переход из летнего времени в стандартное для часового пояса или обратно, то таймдельта игнорирует его. Как-будто не было никакого прыжка на час вперед/назад.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
 
a = datetime(2023, 10, 28, hour=22,
             tzinfo=ZoneInfo('Europe/Berlin'))
print(a)
 
b = a + timedelta(hours=5)
print(b)
2023-10-28 22:00:00+02:00
2023-10-29 03:00:00+01:00

В примере в реальном физическом мире прошло 6 часов, так как часы были переведены на час назад. Поэтому через реальных 5 часов должно было бы быть 2 часа ночи, а не 3.


Даты и время в Python. Курс




Все разделы сайта