Python中timedelta处理夏令时(DST)时段的异常问题咨询
我编写了一个接收定期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)
上周日当地切换夏令时(DST)后,我遇到了一个Bug(原本代码中使用.seconds而非.total_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的timestamp却能正确处理?
- 是否不应优先使用timedelta,转而使用timestamp(但我觉得这种方式不够优雅)?
- 希望不引入pytz这类外部依赖解决该问题。
1. 核心差异原因
Python的无时区信息datetime(naive datetime)本质只是记录年、月、日、时分秒的数值组合,不关联任何时区或DST规则。计算两个naive datetime的timedelta时,Python只会做纯数值减法——比如你例子里的2:20(fold=1)和2:30,数值上前者分钟数更小,所以差值是-10分钟,完全不会考虑这两个时间点在DST切换后的实际先后顺序。
而timestamp()方法会把naive datetime当作本地时间,根据系统时区和DST规则转换成线性的Unix时间戳(从1970-01-01 UTC开始的秒数)。Unix时间戳不受DST切换影响,所以差值能准确反映实际流逝的时间——你例子里的second_date是DST切换后的“第二个2:20”,对应的时间戳比切换前的2:30大50分钟,因此差值是3000秒。
2. 方案选择
如果你的需求是判断实际时间是否流逝了X分钟,用timestamp计算差值是可靠的,虽然直观性不如timedelta,但能彻底规避DST坑。同时也有更优雅的无依赖方案。
3. 无外部依赖的解决方法
方案一:用Unix时间戳记录时间
直接用time.time()记录最后一次ping的时间戳,计算差值时完全不受DST影响:
from datetime import datetime import time from random import randint class Checker: def __init__(self): self.last_ping_ts = time.time() # 记录Unix时间戳 def ping(self): self.last_ping_ts = time.time() def panic_if_stale(self): if time.time() - self.last_ping_ts > 600: raise Exception('PANIC') if __name__ == '__main__': checker = Checker() while True: if randint(0, 10) == 5: checker.ping() checker.panic_if_stale() time.sleep(60)
方案二:用UTC时间记录
UTC没有DST切换,用带时区的UTC时间计算timedelta是安全的(Python 3.6+支持):
from datetime import datetime, timezone import time from random import randint 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: raise Exception('PANIC') if __name__ == '__main__': checker = Checker() while True: if randint(0, 10) == 5: checker.ping() checker.panic_if_stale() time.sleep(60)
内容的提问来源于stack exchange,提问作者teuneboon

