You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 06:45:52