Python 基础体系 · 第 40/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 日期时间:datetime、时区、DST、时间戳和序列化
日期时间问题通常不是“如何把字符串解析成对象”,而是要先回答:
- 这个值表示一个日期、一天中的本地时间,还是一个确定的瞬时点?
- 它是否包含时区信息?
- 计算的是“墙上时钟向后一天”,还是“经过了 24 个小时”?
- 传输和存储时,是否保留了足够的信息,能够恢复原来的语义?
Python 3.14 的标准库把这些概念分布在 datetime、zoneinfo、time 和 json 等模块中。真正可靠的时间处理,关键不在于记住若干格式字符串,而在于区分日历语义、时钟语义和瞬时语义。
一、先建立时间模型
1. date、time、datetime 和 timedelta
datetime 模块提供几类核心对象:
date:年月日,不包含一天中的时间;time:时分秒,不包含具体日期;datetime:日期和时间的组合;timedelta:两个日期时间之间的持续时间;tzinfo:时区规则的抽象接口;timezone:固定 UTC 偏移的时区实现。
date 使用向前、向后扩展的公历模型;time 和 datetime 的分辨率最高为微秒,并且不处理闰秒。datetime 对象的范围是公元 1 年到 9999 年。(docs.python.org)
import datetime as dt
birthday = dt.date(1990, 5, 20)
office_open = dt.time(9, 30)
created_at = dt.datetime(2026, 9, 1, 10, 15, 30)
delay = dt.timedelta(minutes=15)
print(birthday)
print(office_open)
print(created_at + delay)
输出:
1990-05-20
09:30:00
2026-09-01 10:30:30
这里的 timedelta(minutes=15) 表示持续 15 分钟,而不是某个时区中的“下一个 15 分钟刻度”。
timedelta 内部归一化为 days、seconds 和 microseconds 三部分。例如:
delta = dt.timedelta(hours=-1)
print(delta)
print(delta.days, delta.seconds, delta.microseconds)
输出类似:
-1 day, 23:00:00
-1 82800 0
因此:
delta.seconds
并不是总秒数。需要总时长时,应使用:
print(delta.total_seconds())
输出:
-3600.0
这是因为 seconds 始终被规范化到 0 到 86399 之间;它只是归一化表示的一部分。(docs.python.org)
二、naive 和 aware:有没有时区信息还不够
1. naive datetime
没有时区语义的 datetime 称为 naive datetime:
naive = dt.datetime(2026, 9, 1, 12, 0, 0)
print(naive.tzinfo)
输出:
None
但 tzinfo is None 只是最常见的判断方式。严格来说,一个 datetime 只有在以下两个条件同时成立时才是 aware:
tzinfo不为None;tzinfo.utcoffset(datetime)返回非None。
否则就是 naive。(docs.python.org)
naive 对象本身没有说明:
- 它是 UTC;
- 它是系统本地时间;
- 它是上海时间;
- 它是数据库服务器所在时区的时间。
因此下面两个值在语义上都不完整:
dt.datetime(2026, 9, 1, 12, 0)
dt.datetime(2026, 9, 1, 12, 0)
它们看起来相同,但程序无法从对象自身判断它们对应哪个瞬时点。
2. aware datetime
带有明确时区规则或固定偏移的 datetime 称为 aware datetime:
utc_time = dt.datetime(
2026, 9, 1, 12, 0,
tzinfo=dt.UTC,
)
print(utc_time)
print(utc_time.tzinfo)
print(utc_time.utcoffset())
输出:
2026-09-01 12:00:00+00:00
UTC
0:00:00
Python 3.11 引入的 datetime.UTC 是 datetime.timezone.utc 的别名。Python 3.14 中,推荐使用 aware datetime 表示 UTC,而不是使用带有“假定为 UTC”含义的 naive datetime。(docs.python.org)
now_utc = dt.datetime.now(dt.UTC)
不要把以下代码当作“把时间转换成 UTC”:
local = dt.datetime(2026, 9, 1, 12, 0)
wrong = local.replace(tzinfo=dt.UTC)
replace(tzinfo=...) 不会改变时分秒,只会修改标签。它表达的是:
原来这个墙上时间就应该被解释为 UTC。
如果原始值实际是中国标准时间,那么正确的转换应先附加上海时区,再转换到 UTC:
from zoneinfo import ZoneInfo
local = dt.datetime(
2026, 9, 1, 12, 0,
tzinfo=ZoneInfo("Asia/Shanghai"),
)
utc = local.astimezone(dt.UTC)
print(local)
print(utc)
输出:
2026-09-01 12:00:00+08:00
2026-09-01 04:00:00+00:00
二者表示同一个瞬时点。
三、固定偏移和 IANA 时区不是一回事
1. timezone:固定 UTC 偏移
datetime.timezone 适合表示固定偏移:
china = dt.timezone(dt.timedelta(hours=8), name="UTC+08:00")
value = dt.datetime(2026, 9, 1, 12, tzinfo=china)
print(value)
print(value.tzname())
输出:
2026-09-01 12:00:00+08:00
UTC+08:00
固定偏移只回答:
此时比 UTC 早或晚多少?
它不包含某个地区在不同年份如何切换夏令时、历史上是否修改过时区规则等信息。
2. ZoneInfo:IANA 时区数据库
zoneinfo.ZoneInfo 使用 IANA 时区标识,例如:
Asia/ShanghaiAmerica/Los_AngelesEurope/London
from zoneinfo import ZoneInfo
shanghai = ZoneInfo("Asia/Shanghai")
los_angeles = ZoneInfo("America/Los_Angeles")
meeting = dt.datetime(2026, 9, 1, 9, 0, tzinfo=shanghai)
print(meeting)
print(meeting.astimezone(los_angeles))
ZoneInfo 是 tzinfo 的具体实现,能够依据 IANA 时区规则处理偏移变化和 DST。它从系统时区数据库或 tzdata 包读取规则;某些系统,尤其是 Windows,可能没有可用的 IANA 数据库,跨平台项目通常应显式依赖 tzdata。(docs.python.org)
python -m pip install tzdata
代码中的时区名称不应使用含糊缩写:
# 不推荐:EST 可能只表示固定偏移,也可能被不同系统解释不同
"EST"
# 推荐:包含地区和规则
"America/New_York"
UTC+08:00 和 Asia/Shanghai 在当前很多日期上可能得到相同偏移,但它们携带的信息不同:
UTC+08:00:固定偏移;Asia/Shanghai:地区规则,可能包含历史变化。
如果业务只需要计算当前偏移,固定时区足够;如果需要展示当地时间、处理历史数据或安排未来事件,应保存 IANA 时区名称。
四、时区转换的核心:保持瞬时点不变
astimezone(target) 的语义是:
把同一个瞬时点表示成目标时区的本地时间。
例如:
instant = dt.datetime(
2026, 9, 1, 4, 0,
tzinfo=dt.UTC,
)
shanghai = instant.astimezone(ZoneInfo("Asia/Shanghai"))
new_york = instant.astimezone(ZoneInfo("America/New_York"))
print(instant)
print(shanghai)
print(new_york)
输出会类似:
2026-09-01 04:00:00+00:00
2026-09-01 12:00:00+08:00
2026-09-01 00:00:00-04:00
换算关系可以写成:
上海的偏移为 +08:00,所以:
astimezone() 的实现逻辑可以近似理解为:
def convert(value, target):
utc_like = (value - value.utcoffset()).replace(tzinfo=target)
return target.fromutc(utc_like)
也就是说,先把源时间归一到 UTC,再根据目标时区规则生成新的本地表示。Python 文档明确区分了这种“转换”和 replace(tzinfo=...) 这种“直接附加标签”。(docs.python.org)
五、DST:夏令时会制造不存在和重复的本地时间
1. DST 的两类边界
DST,即 Daylight Saving Time,夏令时,是某些地区对本地时钟规则进行季节性调整的制度。
转换时会出现两种特殊区间。
春季前拨:时间不存在
假设某时区在凌晨 2 点把时钟拨到 3 点:
01:59:59
03:00:00
那么 02:30 这个本地时间根本没有对应的瞬时点。
秋季回拨:时间重复
假设时钟从 02:00 拨回 01:00:
00:59:59
01:00:00 # 第一次,夏令时
01:59:59
01:00:00 # 第二次,标准时
01:59:59
02:00:00
此时 01:30 对应两个不同的瞬时点。
2. fold:区分重复时间
PEP 495 为 datetime 增加了 fold 属性:
fold=0:重复区间的较早一次;fold=1:重复区间的较晚一次。
from zoneinfo import ZoneInfo
la = ZoneInfo("America/Los_Angeles")
first = dt.datetime(
2020, 11, 1, 1, 0,
tzinfo=la,
fold=0,
)
second = first.replace(fold=1)
print(first)
print(first.utcoffset())
print(first.timestamp())
print(second)
print(second.utcoffset())
print(second.timestamp())
输出:
2020-11-01 01:00:00-07:00
-1 day, 17:00:00
1604217600.0
2020-11-01 01:00:00-08:00
-1 day, 16:00:00
1604221200.0
两个对象显示相同的本地时分秒,但时间戳相差 3600 秒。
从 UTC 转换到 ZoneInfo 时,Python 会自动设置正确的 fold:
u1 = dt.datetime(2020, 11, 1, 8, tzinfo=dt.UTC)
u2 = dt.datetime(2020, 11, 1, 9, tzinfo=dt.UTC)
print(u1.astimezone(la))
print(u2.astimezone(la))
输出:
2020-11-01 01:00:00-07:00
2020-11-01 01:00:00-08:00
ZoneInfo 对重复时间使用 fold=0 和 fold=1 区分转换前后的偏移。(docs.python.org)
3. fold 不会出现在普通 ISO 字符串中
print(first.isoformat())
print(second.isoformat())
输出:
2020-11-01T01:00:00-07:00
2020-11-01T01:00:00-08:00
这里偏移不同,所以仍可区分。但如果业务只保存:
2020-11-01T01:00:00
那么两个瞬时点都被压缩成了同一个字符串。
更重要的是,isoformat() 通常只保存 UTC 偏移,不保存 ZoneInfo 的 IANA 键名。例如序列化后可以恢复 -08:00,但不能仅凭字符串恢复“这是 America/Los_Angeles 规则下的时间”。
六、日期时间加法:墙上时间和经过时间不是同一个问题
这是日期时间代码中最容易被忽略的边界。
考虑洛杉矶 2020 年秋季回拨:
from zoneinfo import ZoneInfo
la = ZoneInfo("America/Los_Angeles")
start = dt.datetime(2020, 10, 31, 12, tzinfo=la)
next_day = start + dt.timedelta(days=1)
elapsed = (
next_day.astimezone(dt.UTC)
- start.astimezone(dt.UTC)
)
print(start)
print(next_day)
print(elapsed)
输出:
2020-10-31 12:00:00-07:00
2020-11-01 12:00:00-08:00
1 day, 1:00:00
为什么加了 timedelta(days=1),UTC 经过时间却是 25 小时?
因为这段代码表达的是:
在同一个本地时区中,把日历日期向后移动一天,并保留本地时钟 12:00。
秋季回拨使这两个本地中午之间实际经过了 25 个小时。
如果业务要求的是严格经过 24 小时,应在绝对时间轴上计算:
start_utc = start.astimezone(dt.UTC)
after_24h = start_utc + dt.timedelta(hours=24)
display = after_24h.astimezone(la)
print(after_24h)
print(display)
输出:
2020-11-01 19:00:00+00:00
2020-11-01 11:00:00-08:00
两种语义分别是:
本地日历语义:2020-11-01 12:00
绝对持续时间语义:经过 24 小时后是 2020-11-01 11:00
因此:
- “每天当地时间 09:00 执行”是墙上时间语义;
- “每隔 24 小时执行”是持续时间语义;
- “从创建时刻起 30 分钟后超时”应使用绝对时间或单调时钟语义。
Python 文档规定,datetime + timedelta 会保留输入对象的 tzinfo,即使对象 aware,也不会在加法操作中自动把时区转换到另一个对象;两个具有相同 tzinfo 的 aware datetime 在减法时,tzinfo 和 fold 会被忽略。(docs.python.org)
七、当前时间:datetime.now() 和 time 中的时钟
1. 获取当前 UTC 时间
now = dt.datetime.now(dt.UTC)
print(now)
不要使用已经被弃用的:
dt.datetime.utcnow()
Python 3.12 起,utcnow() 已被标记为弃用,推荐使用:
dt.datetime.now(dt.UTC)
因为 utcnow() 返回的是 naive datetime,容易被后续代码误认为本地时间。(docs.python.org)
2. Unix 时间戳
POSIX 时间戳通常表示从:
1970-01-01 00:00:00 UTC
开始经过的秒数,通常不计闰秒。(docs.python.org)
instant = dt.datetime(
2026, 9, 1, 12, 0,
tzinfo=dt.UTC,
)
timestamp = instant.timestamp()
restored = dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
print(timestamp)
print(restored)
对于 aware datetime,时间戳可以形式化为:
datetime.timestamp() 对 naive datetime 的解释不同:naive datetime 会被当作系统本地时间,并依赖平台的 C 时间函数。因此,同一个 naive 值在不同机器上可能产生不同时间戳。(docs.python.org)
naive = dt.datetime(2026, 9, 1, 12, 0)
# 只有在明确知道 naive 表示 UTC 时,才可以这样做
timestamp = naive.replace(tzinfo=dt.UTC).timestamp()
3. 用整数避免浮点精度损失
datetime.timestamp() 返回 float。对于日志排序、数据库主键或高精度事件,浮点数可能造成精度边界问题。
time.time_ns() 返回从 epoch 起算的整数纳秒数,可避免 float 表示带来的精度损失。(docs.python.org)
import time
ns = time.time_ns()
milliseconds = ns // 1_000_000
microseconds = ns // 1_000
print(ns)
print(milliseconds)
print(microseconds)
但要注意:
整数纳秒表示的是存储精度,不等于操作系统时钟真的具有纳秒级准确度。
4. 墙上时钟和单调时钟
time.time() 是系统墙上时钟,可能因为 NTP 校时、管理员调整或虚拟机时间同步而向前或向后变化。
超时和耗时测量应使用单调时钟:
import time
begin = time.monotonic()
# 执行某项操作
time.sleep(0.2)
elapsed = time.monotonic() - begin
print(elapsed)
monotonic() 不能转换成现实日期,但保证适合比较同一进程中的时间间隔。可以这样区分:
| 需求 | 适合的表示 |
|---|---|
| 显示当前时间 | aware datetime |
| 保存事件发生时刻 | UTC datetime 或整数时间戳 |
| 计算业务日历时间 | datetime + ZoneInfo |
| 测量耗时和超时 | time.monotonic() |
| 性能基准 | time.perf_counter() |
time 模块将 time、monotonic、perf_counter、process_time 等时钟作为不同语义的时钟提供。(docs.python.org)
八、解析和格式化:isoformat、fromisoformat、strftime
1. ISO 8601
ISO 8601 是日期时间交换时常用的文本格式。Python 的 datetime.isoformat() 会生成类似:
2026-09-01T12:00:00+08:00
2026-09-01T04:00:00+00:00
示例:
value = dt.datetime(
2026, 9, 1, 12, 0, 0, 123456,
tzinfo=ZoneInfo("Asia/Shanghai"),
)
print(value.isoformat())
print(value.isoformat(timespec="seconds"))
print(value.isoformat(timespec="milliseconds"))
输出:
2026-09-01T12:00:00.123456+08:00
2026-09-01T12:00:00+08:00
2026-09-01T12:00:00.123+08:00
timespec="milliseconds" 是截断,不是四舍五入。(docs.python.org)
2. fromisoformat
text = "2026-09-01T12:00:00+08:00"
value = dt.datetime.fromisoformat(text)
print(value)
print(value.tzinfo)
输出:
2026-09-01 12:00:00+08:00
UTC+08:00
带 Z 的 UTC 文本也可以解析:
value = dt.datetime.fromisoformat("2026-09-01T04:00:00Z")
print(value)
print(value.tzinfo)
输出:
2026-09-01 04:00:00+00:00
UTC
解析结果中的 +08:00 是固定偏移,并不会自动变成:
ZoneInfo("Asia/Shanghai")
如果应用需要地区时区规则,应单独保存并恢复时区键名。
3. strftime 和 strptime
strftime 将对象格式化成字符串:
value = dt.datetime(
2026, 9, 1, 12, 5, 9,
tzinfo=dt.UTC,
)
print(value.strftime("%Y-%m-%d %H:%M:%S%z"))
输出:
2026-09-01 12:05:09+0000
strptime 按指定格式解析:
parsed = dt.datetime.strptime(
"2026-09-01 12:05:09",
"%Y-%m-%d %H:%M:%S",
)
print(parsed)
输出:
2026-09-01 12:05:09
这个结果是 naive,因为输入没有包含时区信息。
对于机器间接口,优先使用明确的 ISO 8601 格式;对于用户界面,才使用面向本地化展示的 strftime。展示字符串和交换格式不应混用。
九、序列化:格式能否完整恢复原语义
1. JSON 默认不支持 datetime
import json
value = {
"created_at": dt.datetime.now(dt.UTC),
}
json.dumps(value)
会抛出:
TypeError: Object of type datetime is not JSON serializable
JSON 只定义对象、数组、字符串、数字、布尔值和 null 等基本结构,不知道 Python datetime 的类型、时区和 DST 语义。标准库提供 default 参数或 JSONEncoder.default() 让应用自行定义转换。(docs.python.org)
def encode_datetime(obj):
if isinstance(obj, (dt.datetime, dt.date, dt.time)):
return obj.isoformat()
if isinstance(obj, dt.timedelta):
return obj.total_seconds()
raise TypeError(f"unsupported type: {type(obj)!r}")
payload = {
"created_at": dt.datetime(
2026, 9, 1, 12, 0,
tzinfo=ZoneInfo("Asia/Shanghai"),
),
"retry_after": dt.timedelta(seconds=30),
}
text = json.dumps(
payload,
default=encode_datetime,
ensure_ascii=False,
)
print(text)
输出类似:
{"created_at": "2026-09-01T12:00:00+08:00", "retry_after": 30.0}
2. 只保存 ISO 字符串的边界
restored = json.loads(text)
created_at = dt.datetime.fromisoformat(restored["created_at"])
print(created_at)
print(type(created_at.tzinfo))
恢复后的时区对象通常是固定偏移时区,而不是原来的 ZoneInfo("Asia/Shanghai")。
这在“只关心瞬时点”的场景通常足够,因为:
created_at.astimezone(dt.UTC)
仍能得到正确的 UTC 时刻。
但如果业务还关心:
- 用户原本选择的地区;
- 未来规则变化;
- 本地日历事件;
- 原始时区名称;
- 重复时间中的
fold;
那么仅保存 ISO 字符串就不够。
3. 用结构化对象保留完整语义
一种更完整的表示方式是同时保存:
{
"local": "2020-11-01T01:00:00",
"zone": "America/Los_Angeles",
"fold": 1
}
解析代码:
def encode_zoned_datetime(value: dt.datetime) -> dict[str, object]:
if value.tzinfo is None:
raise ValueError("expected an aware datetime")
zone = getattr(value.tzinfo, "key", None)
return {
"local": value.replace(tzinfo=None).isoformat(),
"zone": zone,
"offset": value.utcoffset().total_seconds(),
"fold": value.fold,
}
def decode_zoned_datetime(data: dict[str, object]) -> dt.datetime:
local = dt.datetime.fromisoformat(str(data["local"]))
zone = data.get("zone")
if zone:
return local.replace(
tzinfo=ZoneInfo(str(zone)),
fold=int(data.get("fold", 0)),
)
offset = dt.timedelta(seconds=int(data["offset"]))
return local.replace(tzinfo=dt.timezone(offset))
示例:
original = dt.datetime(
2020, 11, 1, 1, 0,
tzinfo=ZoneInfo("America/Los_Angeles"),
fold=1,
)
encoded = encode_zoned_datetime(original)
decoded = decode_zoned_datetime(encoded)
print(encoded)
print(decoded)
print(decoded.timestamp() == original.timestamp())
输出类似:
{'local': '2020-11-01T01:00:00', 'zone': 'America/Los_Angeles', 'offset': -28800.0, 'fold': 1}
2020-11-01 01:00:00-08:00
True
如果系统只需要事件发生时刻,建议保存 UTC:
{
"occurred_at": "2026-09-01T04:00:00Z"
}
如果系统需要“某地每天某时执行”的业务意图,则应保存本地时间和 IANA 时区,而不是只保存一次转换后的 UTC。
十、定时任务中的时间语义
时间处理和定时调度的关系可以用三个问题区分。
1. 任务是按瞬时点触发,还是按本地日历触发?
瞬时点任务
例如:
在
2026-09-01T04:00:00Z执行一次。
可以保存 UTC 时间戳或 UTC ISO 字符串:
run_at = dt.datetime.fromisoformat(
"2026-09-01T04:00:00+00:00"
)
本地日历任务
例如:
每个工作日,上海时间 09:00 执行。
这不是一个固定的 UTC 时间。业务模型应保存:
schedule = {
"zone": "Asia/Shanghai",
"local_time": "09:00:00",
"weekdays": [0, 1, 2, 3, 4],
}
调度器每次根据日期和 ZoneInfo("Asia/Shanghai") 重新计算下一次瞬时点。
2. DST 边界如何处理?
如果目标时区存在 DST,某个本地计划时间可能:
- 不存在:春季跳过;
- 出现两次:秋季重复。
调度器必须明确策略。例如:
不存在的时间:顺延到下一个有效时间
重复的时间:只执行一次
重复的时间:执行两次
不能依赖操作系统或库的隐式选择,因为“执行一次还是两次”是业务语义,而不是单纯的格式转换。
3. 重复执行、锁和补偿
一个可靠的定时任务通常要把以下信息分开:
计划实例 ID
计划本地日期
计划时区
计划本地时间
解析后的瞬时点
执行状态
尝试次数
锁持有者
最后错误
处理流程可以表示为:
flowchart TD
A[读取计划规则] --> B[计算本地候选时间]
B --> C{时间是否存在}
C -- 否 --> D[按策略跳过或顺延]
C -- 是 --> E{是否重复时间}
E -- 是 --> F[依据 fold 和策略生成实例]
E -- 否 --> G[生成唯一实例]
F --> H[转换为 UTC 瞬时点]
G --> H
D --> H
H --> I[持久化计划实例]
I --> J[抢占分布式锁]
J --> K{锁成功}
K -- 否 --> L[其他执行器处理]
K -- 是 --> M[执行任务]
M --> N{执行成功}
N -- 是 --> O[标记完成]
N -- 否 --> P[记录失败并按补偿策略重试]
锁只能解决“同一实例是否允许并发执行”,不能解决:
- 进程执行到一半崩溃;
- 任务已完成但状态未提交;
- 网络超时导致结果不确定;
- 调度器重启后是否补发;
- DST 重复时间是否生成两个实例。
因此,任务实例应有稳定的幂等键,例如:
job_id + scheduled_local_date + fold
对于不使用 DST 的时区,fold 通常为 0;对于可能重复的本地时间,必须把 fold 纳入实例身份,避免两个不同瞬时点被误判为同一个任务。
十一、常见错误及诊断方式
错误一:混合比较 naive 和 aware
naive = dt.datetime(2026, 9, 1, 12)
aware = dt.datetime(2026, 9, 1, 12, tzinfo=dt.UTC)
naive < aware
排序比较会抛出:
TypeError: can't compare offset-naive and offset-aware datetimes
修复方式不是强行删除时区,而是先明确 naive 的真实语义:
# 如果 naive 实际表示 UTC
naive_as_utc = naive.replace(tzinfo=dt.UTC)
print(naive_as_utc == aware)
错误二:把 replace 当作转换
value = dt.datetime(
2026, 9, 1, 12,
tzinfo=ZoneInfo("Asia/Shanghai"),
)
print(value.replace(tzinfo=dt.UTC))
print(value.astimezone(dt.UTC))
输出分别是:
2026-09-01 12:00:00+00:00
2026-09-01 04:00:00+00:00
前者改变了解释,后者保持了瞬时点。
错误三:只保存本地字符串
2026-09-01 09:00:00
这个字符串没有时区,跨机器、跨服务、跨夏令时规则时都可能产生歧义。
至少应保存:
2026-09-01T09:00:00+08:00
如果是地区日历事件,还应保存:
local = 2026-09-01T09:00:00
zone = Asia/Shanghai
错误四:把 timedelta(days=1) 当作 24 小时
在没有 DST 的时区中,两者经常表现相同;在有 DST 的时区中,它们可能不同。
诊断时同时打印本地值和 UTC 值:
def inspect(value: dt.datetime) -> None:
print("local:", value)
print("utc:", value.astimezone(dt.UTC))
print("offset:", value.utcoffset())
print("fold:", value.fold)
print("timestamp:", value.timestamp())
如果本地时间看似只变化一天,而 UTC 时间变化了 23 或 25 小时,说明代码遇到了 DST 边界,必须回到业务语义判断这是正确行为还是缺陷。
十二、一个可复用的边界检查函数
对外部输入,建议尽早验证时间类型和语义:
def require_aware(value: dt.datetime) -> dt.datetime:
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("datetime must be timezone-aware")
return value
def to_utc(value: dt.datetime) -> dt.datetime:
require_aware(value)
return value.astimezone(dt.UTC)
def parse_utc(text: str) -> dt.datetime:
value = dt.datetime.fromisoformat(text)
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("timestamp must include a timezone offset")
return value.astimezone(dt.UTC)
使用:
event_time = parse_utc("2026-09-01T12:00:00+08:00")
print(event_time)
输出:
2026-09-01 04:00:00+00:00
这个函数拒绝以下输入:
2026-09-01T12:00:00
因为它无法判断输入属于哪个时区。与其让错误的时间进入数据库或消息队列,再在下游产生难以追踪的偏移,不如在边界处拒绝不完整数据。
结语:先确定语义,再选择类型
Python 日期时间处理可以归纳为四种不同对象:
date —— 日历日期
time —— 一天中的墙上时间
datetime —— 日期与时间,可带时区
timedelta —— 持续时间
在此基础上,还要区分:
naive datetime —— 没有足够信息定位到瞬时点
aware datetime —— 能定位到明确瞬时点
fixed offset —— 固定 UTC 偏移
ZoneInfo —— 带历史和 DST 规则的 IANA 时区
timestamp —— 从 Unix epoch 起算的数值时间
monotonic clock —— 只用于可靠测量经过时间
工程代码中的关键选择不是“统一使用 UTC”这么简单,而是:
- 事件发生时刻,使用 aware UTC;
- 用户本地日历事件,保存本地时间和 IANA 时区;
- DST 重复区间,明确处理
fold; - 经过时间和超时,使用单调时钟或 UTC 时间轴;
- JSON 序列化,明确精度、偏移、时区名称和恢复策略;
- 定时任务,区分墙上时间计划与固定间隔计划。
当这些语义被明确后,datetime、ZoneInfo、时间戳和序列化格式才会成为可组合的工具,而不是一组容易互相误用的 API。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python 结构化数据:JSON、CSV、TOML、Schema 与精度边界
- 下一篇:Python 正则表达式:匹配模型、分组、回溯、性能和 Unicode
- 延伸:Python 定时任务:时间语义、调度器、重复执行、锁和补偿
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论