Python 3.9: 使用标准库构造夏令时有效时间戳

Python 3.9: Construct DST valid timestamp using standard library


我想仅使用 Python 3.9 中的标准库来构造 DST 有效时间戳,并希望此版本可以实现。

在我的时区“Europe/Berlin”,2020 年的夏令时交叉点是:
2020-03-29 02:00 时钟切换到 03:00(没有小时 2!)
2020-10-25 03:00 时钟切换回 02:00(小时 2 存在两次!)

我的脚本产生以下输出:
三月
2020-03-29 01:59:00+01:00 CET 加 1 小时:2020-03-29 02:59:00+01:00 CET
(应该是 03:59:00 CEST,因为没有小时 2!)

十月
2020-10-25 02:00:00+02:00 CEST 加 1 小时:2020-10-25 03:00:00+01:00 CET
(似乎没问题 - 编辑:应该是 02:00 CET!!!)

下面提供了示例代码。 Windows 用户可能需要“pip install tzdata”才能正常工作。

任何建议将不胜感激!

'''
Should work out of the box with Python 3.9

Got a fallback import statement.

BACKPORT (3.6+)
pip install backports.zoneinfo

WINDOWS (TM) needs:
pip install tzdata
'''

from datetime import datetime, timedelta
from time import tzname
try:
    from zoneinfo import ZoneInfo
except ImportError:
    from backports import zoneinfo
    ZoneInfo = zoneinfo.ZoneInfo


tz = ZoneInfo("Europe/Berlin")
hour = timedelta(hours=1)

print("MARCH")
dt_01 = datetime(2020, 3, 29, 1, 59, tzinfo=tz)

dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")


print("\nOCTOBER")
dt_01 = datetime(2020, 10, 25, 2, 0, tzinfo=tz)
dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")

虽然是counter-intuitive,但也符合预期。有关日期时间算法如何工作的更多详细信息,请参阅 this blog post。这样做的原因是,将 timedelta 添加到 datetime 应该被认为是“将 calendar/clock 提前 X 数量”,而不是“calendar/clock 之后会说什么”这段时间已经过去了”。请注意,第一个问题可能导致时间甚至不在当地时区!

如果你想,“什么 datetime 代表这个 timedelta 代表的时间量过去后的时间?” (你似乎这样做了),你应该做一些等同于转换为 UTC 并返回的操作,如下所示:

from datetime import datetime, timedelta, timezone

def absolute_add(dt: datetime, td: timedelta) -> datetime:
    utc_in = dt.astimezone(timezone.utc)  # Convert input to UTC
    utc_out = utc_in + td  # Do addition in UTC
    civil_out = utc_out.astimezone(dt.tzinfo)  # Back to original tzinfo
    return civil_out

我相信您可以创建一个覆盖 __add__timedelta 子类来为您执行此操作(如果可以的话,我想将类似的东西引入标准库)。

请注意,如果 dt.tzinfoNone,这将使用您的系统本地时区来确定如何进行绝对加法,并且它将 return 一个可感知的时区。 运行 这个在 America/New_York:

>>> absolute_add(datetime(2020, 11, 1, 1), timedelta(hours=1))
datetime.datetime(2020, 11, 1, 1, 0, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=68400), 'EST'))

如果你想让它对原始日期时间进行民用加法,对已知日期时间进行绝对加法,你可以在函数中检查它是否是原始的:

def absolute_add_nolocal(dt: datetime, td: timedelta) -> datetime:
    if dt.tzinfo is None:
        return dt + td
    return absolute_add(dt, td)

此外,需要说明的是,这与 zoneinfo 无关。这一直是 Python 中日期时间的语义,我们无法以 backwards-compatible 的方式改变它。 pytz 确实有点不同,因为添加 pytz 感知日期时间会做错事,并且需要在算术发生后执行 normalize 步骤,pytz 作者决定normalize 应该使用 absolute-time 语义。

absolute_add 也适用于 pytzdateutil,因为它使用适用于所有时区库的操作。