Python在夏令时前后使用时间增量



我有一个接收常规ping的脚本,每当最后一次ping超过10分钟时,它就会抛出错误并进入紧急模式。简化示例:

from datetime import datetime
import time
from random import randint

class Checker:
def __init__(self):
self.last_ping = datetime.now()
def ping(self):
self.last_ping = datetime.now()
def panic_if_stale(self):
if (datetime.now() - self.last_ping).total_seconds() > 600:
# no ping for longer than 10 minutes, we panic
raise Exception('PANIC')

if __name__ == '__main__':
checker = Checker()
while True:
if randint(0, 10) == 5:
# this is not actually random, just simulating
checker.ping()
checker.panic_if_stale()
time.sleep(60)

上周日夏令时在这里发生了变化,我遇到了一个错误(.total_seconds()行是.seconds,这使得错误实际上引发了恐慌,但即使没有它,仍然有奇怪的行为(。经过一些实验,我发现以下情况正在发生:

from datetime import datetime, timedelta
if __name__ == '__main__':
first_date = datetime(2022, 10, 30, 2, 30)
# this should be 50 minutes later, but after the time change:
second_date = datetime(2022, 10, 30, 2, 20, fold=1)
# this prints out -600 seconds:
print((second_date - first_date).total_seconds())
# this prints out 3000 seconds(the correct answer in my opinion):
print(second_date.timestamp() - first_date.timestamp())
# this prints out 85800 seconds, but I used the wrong function so that's kind of whatever:
print((second_date - first_date).seconds)
# this prints out "False", even though it should be True:
print((second_date - first_date) > timedelta(minutes=5))

timedelta是否没有考虑DST,即使datetime显然考虑了(从时间戳差异可以看出(。在可能的情况下使用timedelta不是最好的做法吗?我应该只使用时间戳吗(我觉得有点难看(?最好我不想像pytz那样给这个项目添加外部依赖项。

避免夏令时问题的方法:使用UTC。例如:

from datetime import datetime, timezone
class Checker:
def __init__(self):
self.last_ping = datetime.now(timezone.utc)
def ping(self):
self.last_ping = datetime.now(timezone.utc)
def panic_if_stale(self):
if (datetime.now(timezone.utc) - self.last_ping).total_seconds() > 600:
# no ping for longer than 10 minutes, we panic
raise Exception('PANIC')

一点背景,为什么DST转换在与时间增量算法结合时会引起问题:时间增量算法是壁时间算法。它不考虑fold。持续时间与您在调整为夏令时转换的时钟上看到的相同。另请参阅时区感知日期时间算法的语义。

在示例中使用感知日期时间使这一点更加清晰:

from zoneinfo import ZoneInfo
first_date = datetime(2022, 10, 30, 2, 30, fold=0, tzinfo=ZoneInfo("Europe/Berlin"))
second_date = datetime(2022, 10, 30, 2, 20, fold=1, tzinfo=ZoneInfo("Europe/Berlin"))
print(first_date, second_date)
# 2022-10-30 02:30:00+02:00 2022-10-30 02:20:00+01:00
# WALL TIME: -600 seconds, 02:30:00 h -> 02:20:00 h
print((second_date - first_date).total_seconds())
# -600.0
# still correct; .timestamp considers fold attribute:
print(second_date.timestamp() - first_date.timestamp())
# 3000.0
# or using UTC:
print(
(second_date.astimezone(ZoneInfo("UTC")) - 
first_date.astimezone(ZoneInfo("UTC"))).total_seconds()
)
# 3000.0

最新更新