Летнее время в Python

Вопрос или проблема

Я пишу программу, которая сильно зависит от часовых поясов и пересечений между ними. Две вещи, с которыми я работаю чаще всего, это создание объекта datetime с использованием текущего времени и затем локализация объекта datetime без часового пояса.

Чтобы создать объект datetime с текущим временем в Тихоокеанском часовом поясе, я сейчас делаю так (python 2.7.2+)

from datetime import datetime
import pytz
la = pytz.timezone("America/Los_Angeles")
now = datetime.now(la)

Это правильно в отношении перехода на летнее/зимнее время? Если нет, то, я полагаю, мне следует делать:

now2 = la.localize(datetime.now())

Мой вопрос в том, почему? Может кто-нибудь показать мне случай, когда первое решение неверно, а второе — верно?

Что касается моего второго вопроса, предположим, у меня есть дата и время от вводимых пользователем данных — 1 сентября 2012 года в 8:00 утра в Лос-Анджелесе, штат Калифорния. Правильный способ создать datetime будет таким:

la.localize(datetime(2012, 9, 1, 8, 0))

Если нет, то как мне следует создавать эти datetime?

Из документации pytz:

Предпочтительный способ работы с временем — всегда работать в UTC, переходя на локальное время только при генерации вывода для чтения людьми.

Так что в идеале следует использовать utcnow вместо now.

Предполагая, что по каким-то причинам вы ограничены и должны работать с локальным временем, вы все равно можете столкнуться с проблемой при локализации текущего времени, если вы делаете это в период перехода на летнее время. То же datetime может встретиться дважды: один раз во время летнего времени и снова во время стандартного времени, и метод localize не знает, как разрешить конфликт, если вы не укажете это явно, используя параметр is_dst.

Так что, чтобы получить текущее время UTC:

utc = pytz.timezone('UTC')
now = utc.localize(datetime.datetime.utcnow())

И чтобы преобразовать его в ваше местное время (но только когда это действительно необходимо):

la = pytz.timezone('America/Los_Angeles')
local_time = now.astimezone(la)

Редактировать: как указано в комментариях @J.F. Sebastian, ваш первый пример с использованием datetime.now(tz) будет работать во всех случаях. Ваш второй пример не работает в период перехода на зиму, как я описал выше. Я все равно рекомендую использовать UTC вместо местного времени для всего, кроме отображения.

Первое решение правильное в отношении перехода на летнее/зимнее время, а второе решение — неправильное.

Я дам пример. Здесь, в Европе, когда я запускаю этот код:

from datetime import datetime
import pytz # $ pip install pytz

la = pytz.timezone("America/Los_Angeles")
fmt="%Y-%m-%d %H:%M:%S %Z%z"
now = datetime.now(la)
now2 = la.localize(datetime.now())
now3 = datetime.now()
print(now.strftime(fmt))
print(now2.strftime(fmt))
print(now3.strftime(fmt))

Я получаю следующее:

2012-08-30 12:34:06 PDT-0700
2012-08-30 21:34:06 PDT-0700
2012-08-30 21:34:06 

datetime.now(la) создает datetime с текущим временем в ЛА, плюс информация о часовом поясе для ЛА.

la.localize(datetime.now()) добавляет информацию о часовом поясе к наивному datetime, но не выполняет преобразование часовых поясов; он просто предполагает, что время уже находится в этом часовом поясе.

datetime.now() создает наивный datetime (без информации о часовом поясе) с локальным временем.

Пока вы находитесь в ЛА, разницы не будет заметно, но если ваш код когда-либо будет выполняться где-то еще, вероятно, он не сделает то, что вы хотели бы.

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

EDIT:

В наши дни я бы рекомендовал использовать zoneinfo

from zoneinfo import ZoneInfo
from datetime import datetime, timedelta

dt = datetime(2020, 10, 31, 12, tzinfo=ZoneInfo("America/Los_Angeles"))

pytz был ненадежен в нескольких случаях, как было упомянуто в оригинальном ответе.

ОРИГИНАЛЬНЫЙ ОТВЕТ

Это работает:

# наивный datetime
d = datetime.datetime(2016, 11, 5, 16, 43, 45) 
utc = pytz.UTC # Часовой пояс UTC
pst = pytz.timezone('America/Los_Angeles') # Часовой пояс ЛА

# Преобразуем в UTC timezone aware datetime
d = utc.localize(d) 
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>)

# показываем в часовом поясе ЛА (без преобразования здесь)
d.astimezone(pst) 
>>> datetime.datetime(2016, 11, 5, 9, 43, 45, 
tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
# мы получаем Тихоокеанское дневное время: PDT

# добавляем 1 день к дате UTC
d = d + datetime.timedelta(days=1) 
>>> datetime.datetime(2016, 11, 6, 16, 43, 45, tzinfo=<UTC>)

d.astimezone(pst) # теперь преобразуем в часовой пояс ЛА >>> datetime.datetime(2016, 11, 6, 8, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>) # Переход на летнее время применяется -> мы получаем Тихоокеанское стандартное время PST

Это НЕ работает:

# наивный datetime
d = datetime.datetime(2016, 11, 5, 16, 43, 45) 
utc = pytz.UTC # Часовой пояс UTC
pst = pytz.timezone('America/Los_Angeles') # Часовой пояс ЛА

# преобразуем в UTC timezone aware datetime
d = utc.localize(d) 
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>);

# преобразуем в часовой пояс 'America/Los_Angeles': НЕ ДЕЛАЙТЕ ЭТО
d = d.astimezone(pst) 
>>> datetime.datetime(2016, 11, 5, 9, 43, 45, 
tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>); 
# мы находимся в Тихоокеанском дневном времени PDT

# добавляем 1 день к локальной дате ЛА: НЕ ДЕЛАЙТЕ ЭТО
d = d + datetime.timedelta(days=1)
>>> datetime.datetime(2016, 11, 6, 9, 43, 45, 
tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>); 
# Переход на летнее время НЕ учитывается, мы все еще находимся в PDT времени, а не PST

Заключение:

datetime.timedelta() НЕ УЧИТЫВАЕТ переход на летнее время.

Всегда выполняйте сложение/вычитание времени в часовом поясе UTC.
Преобразуйте в местное время только для вывода / отображения.

На сайте pytz сказано:

К сожалению, использование аргумента tzinfo стандартных конструкторов datetime «не работает» с pytz для многих часовых поясов.

Таким образом, вы не должны использовать datetime.now(la). Я не знаю всех деталей, но некоторые часовые пояса работают по более экзотичным правилам, чем мы привыкли, и код datetime Python не может с ними справиться. Используя код pytz, они должны обрабатываться корректно, поскольку именно это и является целью pytz. Также могут возникнуть проблемы с временными метками, которые происходят дважды из-за перехода на летнее/зимнее время.

Что касается второго вопроса, это точно то, что показывается в документации, так что вы на правильном пути.

Ответ или решение

Ваша задача по работе с временными зонами и переходами на летнее/зимнее время является довольно распространенной. Давайте рассмотрим, как правильно создавать объекты datetime и работать с ними, учитывая детали, которые касаются перехода на летнее время (DST).

1. Создание объекта datetime с учётом временной зоны

Ваш текущий подход к созданию объекта datetime с помощью datetime.now(la) практически правильный, но имеет некоторые нюансы. Как правильно указано в обсуждениях, использование этого метода не учитывает DST для текущего времени. Например:

from datetime import datetime
import pytz

la = pytz.timezone("America/Los_Angeles")
now = datetime.now(la)  # Временная зона будет добавлена автоматически

Тем не менее, datetime.now(la) в основном работает корректно в большинстве случаев. Этот метод правильно возвращает текущее время в заданной временной зоне. Однако в редких случаях, когда время, которое вы пытаетесь локализовать, попадает в зону перехода (например, когда одно и то же время может произойти дважды, одно под стандартным временем, другое – под летним), это может вызвать путаницу.

Лучший подход для работы с текущим временем – это использование UTC:

utc = pytz.timezone('UTC')
now = datetime.now(utc)  # Создаем наивный datetime в UTC
now = now.astimezone(la)  # Переводим его в LA времени

2. Локализация наивного объекта datetime

Теперь перейдем к вашему вопросу о локализации наивного объекта datetime. Если вы хотите создать datetime для 1 сентября 2012 года, 8:00 утра в Лос-Анджелесе, правильный способ будет следующий:

naive_dt = datetime(2012, 9, 1, 8, 0)  # Наивный datetime
localized_dt = la.localize(naive_dt)  # Локализация с учетом DST

Тем не менее, существует потенциальная проблема с локализацией даты и времени, когда вы работаете с переходами на летнее/зимнее время. Например, в 2012 году 1 сентября в Лос-Анджелесе это не обременено переходом на зимнее время, и, следовательно, вы получите правильный результат. Однако, если бы вы пытались локализовать дату, которая была бы во время смены времени – например, 4 ноября 2012 года, 1:30 AM – вам нужно использовать параметр is_dst для правильного указания DST:

ambiguous_dt = datetime(2012, 11, 4, 1, 30)  # Сомнительный случай из-за перехода
localized_dt = la.localize(ambiguous_dt, is_dst=True)  # Указываем, что это время с DST

3. Рекомендации по работе с временными зонами

Согласно рекомендациям из документации pytz, лучше всегда работать с временем в UTC и конвертировать его в локальное время только для отображения, чтобы избежать путаницы, связанной с переходами на летнее/зимнее время.

Пример кода, учитывающий все аспекты:

import pytz
from datetime import datetime

# Создаем часовой пояс Лос-Анджелеса
la = pytz.timezone("America/Los_Angeles")

# Работая с текущим временем в UTC
utc_now = datetime.now(pytz.utc)
local_time = utc_now.astimezone(la)
print("Теперь в Лос-Анджелесе:", local_time)

# Локализация наивного времени
naive_dt = datetime(2012, 11, 4, 1, 30)
localized_dt = la.localize(naive_dt, is_dst=True)  # Указываем на летнее время
print("Локализованное время с учетом DST:", localized_dt)

Заключение

Используйте UTC для всех внутренних операций с датой и временем, и локализуйте только в том случае, если это необходимо для отображения. Также не забывайте внимательно обращаться с методами localize и astimezone, чтобы избежать путаницы, связанной с переходами на летнее и зимнее время. Если вы обновляете вашу программу до более новой версии Python (3.9 и выше), рассмотрите использование встроенного модуля zoneinfo, который предоставляет альтернативный способ работы с временными зонами и может быть более эффективным.

Оцените материал
Добавить комментарий

Капча загружается...