Вопрос или проблема
Я пишу программу, которая сильно зависит от часовых поясов и пересечений между ними. Две вещи, с которыми я работаю чаще всего, это создание объекта 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?
Предпочтительный способ работы с временем — всегда работать в 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
, который предоставляет альтернативный способ работы с временными зонами и может быть более эффективным.