按人求和与按列求和,差别只在先动哪个下标。 时间差必须先全换成秒再减:14:05 减 10:20 会得到 4 小时 -15 分。 2038 年问题是 32 位装不下秒数:过了那一秒,时间会翻回 1901 年。
这一篇是两个不相干的题凑在一起,但共用一条思路:先统一单位,再动算术。 教材把 2038 年放在这里当专题,因为它跟「时间换成秒」是同一个动作的两面:一秒一秒数很方便, 数到装不下就出事。 这是第 4 章唯一一个 ⭐⭐⭐,难的不是代码,是「32 位」这个前提。
一、按人求和与按列求和
一张三人四科的成绩表。按人求和是「每个人四科加起来」,按列求和是「每科三个人加起来」。 代码上只差一件事:哪个下标在外层。外层是人、内层是科目,就是按人;换成外层科目,就是按列。 演示里按列算出来:语文 235、数学 230、英语 230、物理 235。语文和物理并列最高,数学和英语并列最低。
注意「并列」这个词。两组数碰巧一样,不是算错了——这一句要能一眼看出,否则很容易以为代码有 bug。 这条规律值得单独记:换一个下标先动,就换了一个统计口径。后面做报表、做透视表,用的都是这一句。
二、时间差为什么要先换成秒
从 10:20:30 到 14:05:15,中间多久? 直接减:14 减 10 得 4 小时,5 分减 20 分得 -15 分,15 秒减 30 秒得 -15 秒。 两个负数,还得借位。 演示的做法是先全换成秒:起点 37230 秒,终点 50715 秒,相差 13485 秒, 再拆回 3 小时 44 分 45 秒。
换成秒之后就没有借位问题了——秒是同一个单位,加减随便做。拆的时候才需要按 3600 和 60 分。 这一步「统一单位再算」是本课最通用的技巧之一:角度换弧度、钱换分、时间换毫秒、百分比换小数, 都是同一个道理。
三、2038 年:32 位装不下秒数
秒数这么好用,很多系统干脆就用「从 1970-01-01 起过了多少秒」来记时间。 问题在于存这个秒数的变量有多大。32 位有符号整数最大是 2147483647。 把这个数当秒数换算,得到 2038-01-19 03:14:07(UTC)。再过一秒就装不下了。
装不下会怎样?在 C 的 time_t 这类定义上,它会翻成负数——时间从 2038 年一下退到 1901 年。
这就是「2038 年问题」。
演示里用 struct.pack 试着把 2147483648 装进 32 位整数,
报错原文是 'i' format requires -2147483648 <= number <= 2147483647。
这个报错就是「装不下」的样子。
四、为什么 Python 上不会炸,但问题是真的
在 Python 里直接算 2147483648 秒,一点事没有——它内部用 64 位甚至更大存整数。
所以演示里会出现两行看着矛盾的结果:struct.pack 报错,
而 fromtimestamp(2147483648) 正常给出 2038-01-19。
这不矛盾:一个在试 32 位的容器,一个在用 Python 自己的时间类型。容器小才报错。
所以「2038 年问题」不是 Python 的问题,是那些字段定义成 32 位整数的系统的问题: 老数据库的 timestamp 列、C 语言的 time_t、老设备的固件。 新系统大多换 64 位了,但老设备和老数据表还在。 这也是为什么到了今天还得知道这件事——该查的还是要查。
五、演示里那个 8 小时不是 bug
演示打出两个时间:UTC 是 03:14:07,本机是 11:14:07。 差 8 小时是因为这台机器在东八区。教材和大部分资料写的 03:14:07 是 UTC 时间。 这类差异在跨时区协作里天天出现:同一行日志,两个人看到的时间不一样,先别怀疑代码,先看时区。
这也是「结论要连环境一起说」的一个实例:报时间必须带时区,否则那个数没有意义。
六、动手 Demo:按列求和,再把两个时刻的差算出来
先拿三人四科的成绩表,按人求和、按列求和各算一遍。 再把 10:20:30 到 14:05:15 这段间隔换成秒来减,最后拆回时分秒。 最后试一下 2038 年那个秒数:32 位装不下,而 Python 自己能算。
grid = [[80, 90, 85, 75], [70, 75, 78, 82], [85, 65, 67, 78]]
subs = ['语文', '数学', '英语', '物理']
for j, s in enumerate(subs):
col = sum(grid[i][j] for i in range(len(grid)))
print(s, col, round(col / len(grid), 1))
def to_sec(h, m, s):
return h * 3600 + m * 60 + s
a = to_sec(10, 20, 30)
b = to_sec(14, 5, 15)
d = b - a
print(a, b, d, '->', d // 3600, '小时', d % 3600 // 60, '分', d % 60, '秒')
import struct
print(struct.pack('<i', 2147483647).hex())
try:
struct.pack('<i', 2147483648)
except struct.error as e:
print('装不下:', e)
实跑输出:
--------------------------------------------------------------------
合计与时间差:列求和,以及 2038 年那件事
--------------------------------------------------------------------
① 按科目合计:列求和
--------------------------------------------------------------------
| 科目 | 三人合计 | 平均 |
|---|---:|---:|
| 语文 | 235 | 78.3 |
| 数学 | 230 | 76.7 |
| 英语 | 230 | 76.7 |
| 物理 | 235 | 78.3 |
按人行求和是 sum(grid[i]),按列求和是 sum(grid[i][j] for i)。
换一个下标先动,就换了一个统计口径。
② 时间差:先全换成秒
--------------------------------------------------------------------
从 10:20:30 到 14:05:15
起点 37230 秒,终点 50715 秒,相差 13485 秒
拆回去:3 小时 44 分 45 秒
为什么不直接小时减小时、分钟减分钟?因为 14:05 减 10:20 会得到 4 小时 -15 分。
换成秒就没有借位问题——秒是同一个单位,加减随便做。
这一步「统一单位再算」是本课最通用的技巧之一:角度换弧度、钱换分、时间换毫秒,都是同一个道理。
③ 秒数会溢出:2038 年那件事
--------------------------------------------------------------------
32 位有符号整数最大是 2147483647
把它当秒数(从 1970-01-01 算起):
UTC 2038-01-19 03:14:07
本机(东八区)2038-01-19 11:14:07
! 这两个数差 8 小时,是时区,不是 bug。教材和大部分资料写的是 03:14:07,那是 UTC;
这台机器在东八区,所以显示 11:14:07。
那再加一秒呢?先试试用 32 位整数装它:
struct.error: 'i' format requires -2147483648 <= number <= 2147483647
但 Python 自己用 64 位存时间,直接算不会错:
fromtimestamp(2147483648) -> 2038-01-19
所以「2038 年问题」不是 Python 的问题,是那些字段定义成 int32 的系统的问题:
老数据库的 timestamp 列、C 语言的 time_t、老设备的固件。
过了那一秒,32 位的秒数会翻成负数,时间退回 1901 年。
新系统大多换 64 位了,但老设备和老数据表还在,该查的还是得查。
①按列求和那句 sum(grid[i][j] for i in range(len(grid))),外层是行下标 i,
但固定了列 j——这就是「按列」的形状。
②to_sec 把时分秒压成一个数。三个数的借位问题一下就没了,这是这一篇最值得带走的一招。
③拆回时分秒用的是 // 和 %:先整除 3600 得小时,余下的再整除 60 得分。
和 2.1 讲的「能分几组、剩几个」是同一对运算。
④struct.pack('<i', ...) 里的 i 指 32 位有符号整数。
2147483647 能装,加一就报错——这一行就是 2038 年问题的实物。
30 秒实验
- 把三人四科换成四人三科,看按列求和的代码要不要改(提示:几乎不用)。
- 算一下 2026-10-31 到 2027-01-01 差多少秒,再拆回天、时、分。
- 把
'<i'换成'<q'(64 位),看 2147483648 还报不报错。
常见误区
- 「时间差直接小时减小时、分钟减分钟。」会减出负数,还得借位。先换成秒就没有这问题。
- 「语文和物理都是 235,肯定算错了。」并列很正常,值是算出来的,不是猜的。
- 「Python 能算 2038 年之后,所以 2038 问题不存在。」老数据库和老设备的字段还是 32 位。
自测三问(点开对答案)
1. 按人求和与按列求和,代码差在哪里? —— 差在外层循环走哪个下标。外层走人就是按人,外层走科目就是按列。 2. 时间差为什么先换成秒? —— 换成秒之后是同一个单位,加减没有借位问题。拆的时候再按 3600 和 60 分。 3. 2038 年问题到底是什么问题? —— 32 位有符号整数装不下 2038-01-19 之后的秒数,会翻成负数退回 1901 年。第 4 章最后一节讲两个小技巧:交换两个变量的值,和求最大公约数。 前者是面试常客,后者是「有穷性」最好的例子。
- ☐ (本篇完,下一篇 4.5 交换变量与最大公约数)