本节目标:分清时间戳、naive/aware 日期与带时区日期,用 zoneinfo 正确处理夏令时,避开 replace 与 astimezone 的经典陷阱。
适用版本:Python 3.12+(实测 3.14.6)
11.1 datetime、zoneinfo 与时区
10.3 节我们把数据序列化成 JSON,可一旦数据里带上时间,麻烦就来了:"2026-09-11T10:00:00" 到底是北京时间还是 UTC?本节把「时间」这件事从头理清——这是后面写日志、做命令行工具、处理接口时间戳的共同基础。
11.1.1 两种「时间」:时间戳与结构化日期
Python 里表示时间有两套并行的模型。第一套是 time 模块的时间戳(Unix epoch 秒):
import time
print(time.time()) # 从 1970-01-01 UTC 起的浮点秒
print(time.time_ns()) # 整数纳秒,无浮点误差
1791502397.5738492
1791502397573856000
时间戳是一个绝对量:全世界任何一台机器读到它,指向的都是同一个瞬间。它的缺点是「不好读」,你没法一眼看出这是几月几号。
第二套是 datetime 模块的结构化对象,由年、月、日、时、分、秒等字段组成,打印出来是 2026-09-11 10:30:00 这样直观的形态,还能用 .year、.month、.hour 等属性取出各个字段。一个 datetime 对象背后仍对应一个时间戳,但只有在它知道自己属于哪个时区时,这个映射才是确定的。这就引出了本节最核心的一组概念。
11.1.2 naive 与 aware:差一个时区信息
datetime 对象分两类:
- naive(朴素):没有时区信息,
tzinfo为None。 - aware(带时区):
tzinfo不为None,能唯一确定一个瞬间。
from datetime import datetime, timezone
naive = datetime(2026, 9, 11, 10, 0, 0)
aware = datetime(2026, 9, 11, 10, 0, 0, tzinfo=timezone.utc)
print(naive, "| tzinfo:", naive.tzinfo)
print(aware, "| tzinfo:", aware.tzinfo)
print("naive.timestamp() =", naive.timestamp())
print("aware.timestamp() =", aware.timestamp())
2026-09-11 10:00:00 | tzinfo: None
2026-09-11 10:00:00+00:00 | tzinfo: UTC
naive.timestamp() = 1789092000.0
aware.timestamp() = 1789120800.0
两者相差整整 28800 秒(8 小时)——因为 naive.timestamp() 把 10:00 当成了本机时区(本机为 UTC+8)来解释。naive 对象的含义取决于运行环境,换一台时区不同的机器,同一个对象就指向不同的瞬间。这是无数线上事故的根源。
铁律:跨进程、跨机器、存数据库、进日志的时间,一律用 aware 对象。 naive 只适合「本地展示」这种不需要换算的场景。
11.1.3 now() 的三种写法与 utcnow() 的弃用
获取当前时间有三种常见写法,行为并不相同:
from datetime import datetime, timezone
print("now() ->", datetime.now())
print("now(UTC) ->", datetime.now(timezone.utc))
print("utcnow() ->", datetime.utcnow(), "(naive!)")
now() -> 2026-10-09 07:39:28.772965
now(UTC) -> 2026-10-08 23:39:28.773094+00:00
utcnow() -> 2026-10-08 23:39:28.778016 (naive!)
| 写法 | 返回 | 评价 |
|---|---|---|
datetime.now() | naive,本机时区的墙上时间 | 只适合本地展示 |
datetime.now(timezone.utc) | aware,UTC | 推荐 |
datetime.utcnow() | naive,值是 UTC 但没带 tzinfo | 已弃用,别再用 |
utcnow() 看似方便,但它返回的是 naive 对象——值虽然是 UTC,却没有任何标记说明这一点,很容易被误当成本地时间。从 3.12 起它已被标记为弃用,3.14.6 实测会打出警告:
DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for
removal in a future version. Use timezone-aware objects to represent datetimes
in UTC: datetime.datetime.now(datetime.UTC).
正确写法是 datetime.now(timezone.utc),或者 3.11 起更短的别名 datetime.now(datetime.UTC)(datetime.UTC 就是 timezone.utc)。
11.1.4 zoneinfo:标准库自带的时区库
要处理「北京时间」「纽约时间」,就需要具体的时区规则,而这些规则来自 IANA 时区数据库。Python 3.9 起标准库内置了 zoneinfo,无需再装第三方 pytz:
from datetime import datetime
from zoneinfo import ZoneInfo
sh = ZoneInfo("Asia/Shanghai")
ny = ZoneInfo("America/New_York")
print(datetime(2026, 9, 11, 10, 0, tzinfo=sh))
print(datetime(2026, 9, 11, 10, 0, tzinfo=ny))
2026-09-11 10:00:00+08:00
2026-09-11 10:00:00-04:00
时区名用 "洲/城市" 形式(如 Asia/Shanghai、Europe/London、America/New_York),不要用 "CST" 这种缩写——它至少能指代中国标准时间、美国中部时间、古巴标准时间三个互相矛盾的时区。
11.1.5 夏令时边界实测
夏令时(DST)是时区问题的重灾区。以美国纽约为例,2026 年 3 月 8 日凌晨 2 点直接跳到 3 点(少一小时),11 月 1 日凌晨 2 点又拨回 1 点(多一小时)。zoneinfo 会自动套用这套规则:
from datetime import datetime
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
for label, dt in [
("2026-03-08 01:30", datetime(2026, 3, 8, 1, 30, tzinfo=ny)),
("2026-03-08 03:30", datetime(2026, 3, 8, 3, 30, tzinfo=ny)),
("2026-11-01 00:30", datetime(2026, 11, 1, 0, 30, tzinfo=ny)),
("2026-11-01 02:30", datetime(2026, 11, 1, 2, 30, tzinfo=ny)),
]:
print(label, "->", dt, "| utcoffset:", dt.utcoffset(), "| dst:", dt.dst())
2026-03-08 01:30 -> 2026-03-08 01:30:00-05:00 | utcoffset: -1 day, 19:00:00 | dst: 0:00:00
2026-03-08 03:30 -> 2026-03-08 03:30:00-04:00 | utcoffset: -1 day, 20:00:00 | dst: 1:00:00
2026-11-01 00:30 -> 2026-11-01 00:30:00-04:00 | utcoffset: -1 day, 20:00:00 | dst: 1:00:00
2026-11-01 02:30 -> 2026-11-01 02:30:00-05:00 | utcoffset: -1 day, 19:00:00 | dst: 0:00:00
可以看到偏移量在 -05:00(标准时间)和 -04:00(夏令时)之间自动切换。作为对照,中国不实行夏令时,Asia/Shanghai 全年固定 +08:00,dst() 恒为 0:00:00。
11.1.6 replace 与 astimezone:一字之差的经典 bug
要把一个时间「换成另一个时区」,新手常写 replace(tzinfo=...),这是错误的。两者的区别是本节的必考项:
from datetime import datetime
from zoneinfo import ZoneInfo
sh = ZoneInfo("Asia/Shanghai")
ny = ZoneInfo("America/New_York")
meeting = datetime(2026, 3, 8, 15, 0, tzinfo=sh)
print("原始 (上海):", meeting)
print("replace(ny):", meeting.replace(tzinfo=ny), " <-- 时刻没变,只贴了新标签")
print("astimezone(ny):", meeting.astimezone(ny))
print("时刻相等?", meeting == meeting.replace(tzinfo=ny), "|", meeting == meeting.astimezone(ny))
原始 (上海): 2026-03-08 15:00:00+08:00
replace(ny): 2026-03-08 15:00:00-04:00 <-- 时刻没变,只贴了新标签
astimezone(ny): 2026-03-08 03:00:00-04:00
时刻相等? False | True
replace(tzinfo=ny):只改标签,不改时刻。它把「上海下午 3 点」硬说成「纽约下午 3 点」,实际上是把时间平移了 12 小时。astimezone(ny):保持时刻不变,换算成纽约当地应该显示的时刻(凌晨 3 点)。这才是你想要的。
一句话记忆:replace 是「改字段」,astimezone 是「换算」。 要换时区,永远用 astimezone。
11.1.7 aware 日期做加减也会骗你
即便用上了 aware 对象,+ timedelta 也未必是「绝对时间」的加减。看这段跨越夏令时的代码:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
before = datetime(2026, 3, 7, 12, 0, tzinfo=ny)
after = before + timedelta(days=1)
print("before:", before)
print("after :", after)
print("wall-clock 相减:", after - before)
print("按时间戳算真实间隔:", (after.timestamp() - before.timestamp()) / 3600, "小时")
before: 2026-03-07 12:00:00-05:00
after : 2026-03-08 12:00:00-04:00
wall-clock 相减: 1 day, 0:00:00
按时间戳算真实间隔: 23.0 小时
before + timedelta(days=1) 保持的是墙上时间(都是中午 12 点),而不是同一瞬间加 24 小时。因此 after - before 说「1 天」,实际却只过了 23 小时。要计算精确的时长,先把两端都换到 UTC 再相减。
11.1.8 ISO 8601 与 fromisoformat
在系统之间传递时间,通用格式是 ISO 8601。isoformat() 负责输出,fromisoformat() 负责解析:
from datetime import datetime
now = datetime(2026, 9, 11, 10, 30, 15, 123456)
print(now.isoformat())
print(now.isoformat(timespec="seconds"))
2026-09-11T10:30:15.123456
2026-09-11T10:30:15
3.11 起 fromisoformat() 大幅增强,能解析的格式比过去多得多:
from datetime import datetime, date
samples = [
"2026-09-11T10:30:15+08:00",
"2026-09-11 10:30:15", # 空格分隔
"2026-09-11T10:30:15Z", # Z 后缀表示 UTC
"2026-09-11", # 只有日期
"20260911T103015", # 紧凑基本格式
]
for s in samples:
print(f"{s!r:32} -> {datetime.fromisoformat(s)!r}")
print(date.fromisoformat("20260911"))
'2026-09-11T10:30:15+08:00' -> datetime.datetime(2026, 9, 11, 10, 30, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=28800)))
'2026-09-11 10:30:15' -> datetime.datetime(2026, 9, 11, 10, 30, 15)
'2026-09-11T10:30:15Z' -> datetime.datetime(2026, 9, 11, 10, 30, 15, tzinfo=datetime.timezone.utc)
'2026-09-11' -> datetime.datetime(2026, 9, 11, 0, 0)
'20260911T103015' -> datetime.datetime(2026, 9, 11, 10, 30, 15)
2026-09-11
带 +08:00 或 Z 的字符串会解析出 aware 对象,不带时区的则解析成 naive 对象——这一点务必留意。
11.1.9 timedelta、strftime 与常用格式码
timedelta 表示一段时长,可正可负,支持各种单位:
from datetime import datetime, timedelta, timezone
t1 = datetime(2026, 9, 11, 10, 0, tzinfo=timezone.utc)
t2 = datetime(2026, 9, 11, 22, 30, tzinfo=timezone.utc)
print("差值:", t2 - t1, "=", (t2 - t1).total_seconds(), "秒")
print("t1 + 90min:", t1 + timedelta(minutes=90))
print("t1 + 1.5天:", t1 + timedelta(days=1, hours=12))
差值: 12:30:00 = 45000.0 秒
t1 + 90min: 2026-09-11 11:30:00+00:00
t1 + 1.5天: 2026-09-12 22:00:00+00:00
strftime() 把时间格式化成字符串,最常用的格式码如下:
| 码 | 含义 | 示例 |
|---|---|---|
%Y %m %d | 年-月-日 | 2026 09 11 |
%H %M %S | 时:分:秒(24 小时制) | 14 05 09 |
%I %p | 12 小时制与 AM/PM | 02 PM |
%A %a | 星期全称 / 缩写 | Friday Fri |
%j | 一年中的第几天 | 254 |
%z %Z | 时区偏移 / 名称 | +0800 CST |
例如 datetime(2026, 9, 11, 14, 5, 9).strftime("%Y-%m-%d %H:%M:%S") 得到 2026-09-11 14:05:09;对带 Asia/Shanghai 时区的对象,strftime("%a %b %d %z %Z") 得到 Fri Sep 11 +0800 CST。注意 %z 和 %Z 只有 aware 对象才有内容,naive 对象会输出空字符串。
11.1.10 闰秒与单调时钟
两个容易被忽略的细节:
闰秒。POSIX 时间戳约定每天固定 86400 秒,并不建模闰秒,所以 datetime(2016, 12, 31, 23, 59, 60) 会直接抛 ValueError: second must be in 0..59, not 60。
测量耗时要用单调时钟。time.time() 会被 NTP 校时或手动改表影响,可能倒退;测量代码耗时应该用 time.monotonic()(或 time.perf_counter()),它只会单调递增,且没有「当前是几点」的语义:
import time
start = time.monotonic()
sum(range(1_000_000))
print("耗时(ms):", round((time.monotonic() - start) * 1000, 3))
耗时(ms): 8.211
小结
- 时间戳(
time.time())是绝对量但不好读;datetime好读,但只有 aware 对象才对应确定的瞬间。 datetime.now(timezone.utc)是获取 UTC 当前时间的推荐写法;utcnow()返回 naive 对象,自 3.12 起已弃用。- 时区用
zoneinfo.ZoneInfo("洲/城市"),它会自动处理夏令时;Asia/Shanghai全年固定 +08:00。 replace(tzinfo=...)只改标签不改时刻(经典 bug),换时区必须用astimezone();aware 对象的+ timedelta也是墙上时间运算。- 传输用 ISO 8601,
fromisoformat()自 3.11 起能解析更多格式;测耗时用time.monotonic()。
本节把「时间」讲清了,下一节要处理的正是它的孪生问题:程序运行时产生的日志也带时间戳,而且需要分级、定向、可关闭。想先看更全面的时间处理清单,可延伸阅读 Python datetime 完全指南 。
阅读导航:上一节:JSON / CSV / TOML 与序列化安全 · 下一节:logging 与结构化日志 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。