AI-NATIVE 系列 · 四龄数论 · 第4集/共4集 · 20岁
数与表示:程序员入门的第一个抽象(20岁篇)
速览要点
内容简介 这是一篇写给二十岁左右、正在或即将写代码的工程师的随笔。小时候用苹果和石头体会到“数不是名字”,其实正是抽象数据类型、编码、浮点数和 Unicode 数字背后的同一个核心抽象。
为什么值得关注 程序里的 bug,往往就潜伏在数值和编码之间的那道分界线上:比如 0.1 + 0.2 算出来不是 0.3;同一个字节,有人解读成 251,有人解读成 −5;表格里某一列,其实是没有零的 26 进制。每天写代码,都要判断一个事实究竟属于哪一层。
读完你能做什么 试着写一个转换器,让每个数字能在三种编码之间来回转换,并通过不变性测试;在自己的代码里找出一个依赖表示方式的 bug;最后,把位值制唯一性证明讲给一个 15 岁的孩子听。
Numbers at Four Ages — 系列脉络
▶ 看它实际运行:打开在线演示 公开 · 无需登录
- 7岁 · 什么是“数”? 第 1 集
- 11 岁 · 为什么数字有这么多种写法? 第 2 集
- 15 岁 · 十个符号,写遍所有数字 第 3 集
- 20 岁 · 数字与表示 本集
每个程序员为什么都得分清数字和它的表示?
本集不讲位值制唯一性证明;那部分内容请看 第 3 集(15 岁)。连续量(测量而非计数)是下一季的主题。
本集讲什么:数字与表示
这是一篇写给二十岁左右、已经或即将写代码的读者的工程师随笔,聊聊数值和它的书写或存储方式有何不同。你只需要一台装有 Python 的电脑,和十五分钟时间来动手练习。
这是四集中的第 4 集,也为整个系列画上句号。
- 第 1 集 带 7 岁的孩子发现,五个苹果和五块石头其实有着同一个“东西”,“5”不过是它的名字。
- 第 2 集 让 11 岁的孩子自己发明计数系统,亲眼看到位值制为何最终胜出。
- 第 3 集 给 15 岁的孩子展示了:无论有没有零,每种进制下,每个数都只有唯一一种写法。
本集要说的,其实就是:孩子当年悟到的那个道理,正是每个程序员最早学会的抽象。思路来自一章,作者是本系列联合作者陈诗峰,主题是数与数的表示。再往前推一步,这些想法就变成了你可以直接在自己代码里跑的测试。
整个系列都用厨房里的一个画面串联,像爬楼梯一样层层递进:碗里的鸡蛋、餐馆、快餐库房。现在我们爬到最顶层——全国的食品供应链。一箱 42 盒鸡蛋要经过十几个系统:条形码用一种方式编码,仓库数据库存成二进制整数,CSV 导出写成字符串“0042”,表格里则放在 AP 列。数量始终没变,但每个系统的写法都不一样。供应链里的 bug,恰恰就出现在这些差异之间。
在继续往下读之前。 你最后要带 15 岁的孩子一起推一遍第 3 集的唯一性证明。请带着“我要讲明白给别人听”的心态来读。39 项研究发现,如果学习时预期要教别人,教学后的得分提升为 g = 0.48,而没有这种预期时提升为 g = −0.02 (Kobayashi (2024), Educational Psychology Review (ERIC EJ1414484)).
活动 1 · 单手数到 31(先自己想办法)
如果每根竖起的手指代表 1,你的手能表示 0 到 5。别急着往下看,先自己想一个办法,用一只手数到 31,要求 0 到 31 每个数都有独特的手指组合,且不会重复。
然后用你的方法比划出 19 和 31。把你发明的规则写下来。
如果你让每根手指的数值是前一根的两倍(1、2、4、8、16),你就发明了二进制。19 = 16 + 2 + 1,31 就是五指全开。同一只手、同样的手指数,表示范围却扩大了六倍。你只是换了表示法,数本身一点没变。这就是整篇文章的精髓,用一个手势就能说明白。
“相同的数”靠配对定义;表示法就是编码
第 1 集的苹果游戏,并不是小孩专属的简化版。它其实就是 Frege 构建算术时对“相同的数”的定义。在数学基础里,两个集合“有相同的数”,当且仅当它们之间存在一一对应。这就是休谟原则(Hume's Principle),Frege 1884 年算术体系的根基(Zalta, Stanford Encyclopedia of Philosophy, "Frege's Theorem and Foundations for Arithmetic"(中译:Zalta,斯坦福哲学百科全书,《Frege 定理与算术基础》))。这个定义里,完全没提到数字符号。
A 表示法,就是对这个值的编码:用某种字母表上的有限字符串,加上一套读法规则。十进制、二进制、罗马数字、正字计数法、寄存器里的比特,这些都是不同的表示法。软件工程里也有同样的区分:抽象数据类型,强调“值的行为”与“存储方式”分离,这一思想最早由1974 (Liskov & Zilles (1974), "Programming with abstract data types", ACM SIGPLAN Notices提出)。数字本身就是最原始的抽象数据类型,而各种记数系统则是它的不同实现。
在所有编码方式下都成立的性质,被称为不变量。分辨哪些是数值不变量,哪些只是编码层面的事实,是本文要训练的能力。你现在就可以去Number Playground试一试——这是一个免费工具,可以用九种方式展示同一个数字,还能玩陈老师设计的排序小游戏。
本系列的核心观点一句话说清:数字是值,表示是编码,写代码时只依赖值才是正道。
位值制为什么能胜出:用固定字母表实现对数长度的编码
陈老师那一章,其实就是一部“压缩史”,工程师可以用精确的语言总结。
- 计数棒法写 n 就得画 n 个记号:长度随数值线性增长。
- 加法制(如罗马、埃及)虽然更短,但数变大就得造新符号。埃及人有一百万的专用符号,但再往上就没法写了:走不通。
- 位值制在 b 进制下只需要1 + ⌊log_b n⌋个数字(见Weisstein, MathWorld, "Digit")。长度随数值呈对数增长,而且字母表始终不用扩展。
陈老师有一句话:位值制把“无限多的符号”变成了“无限多的位置”。
这种优势是有代价的:每一位都得占着,包括空位,这就是占位零的意义。但其实零并非绝对不可或缺。bijective(无零)k 进制的数字从 1 到 k。每个正整数依然只有唯一一个字符串,空字符串表示零。其实你每天都在用:表格的列名从 A 到 Z,然后是 AA、AB……,这就是 bijective 26 进制。Excel 支持16,384列,最后一列是 XFD(Microsoft, "Excel specifications and limits"(中译:微软,《Excel 规格与限制》)): 24 × 26² + 6 × 26 + 4 = 16,384.
活动 2 · 写个转换器,再试试哪些东西不能变
在看下面的代码前,先自己动手写三个编码器和解码器:带零的 b 进制、无零十进制(数字 1 到 9,十用 κ 表示,陈诗峰的符号),以及表格列标签。然后写性质测试,确保每种编码都能无损往返,也能揪出那些只在某种表示下才成立的“规律”。
下面这个版本,所有断言都能通过:
DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz"
def to_base(n, b):
if n == 0:
return "0"
out = ""
while n:
n, r = divmod(n, b)
out = DIGITS[r] + out
return out
def from_base(s, b):
return int(s, b)
def to_zero_free_ten(n): # digits 1-9 and κ (ten); no 0
out = ""
while n:
r = n % 10 or 10
out = "123456789κ"[r - 1] + out
n = (n - r) // 10
return out
def from_zero_free_ten(s):
n = 0
for ch in s:
n = n * 10 + "123456789κ".index(ch) + 1
return n
def column_label(n): # spreadsheet columns: zero-free base 26
out = ""
while n:
r = n % 26 or 26
out = chr(64 + r) + out
n = (n - r) // 26
return out
def column_number(s):
n = 0
for ch in s:
n = n * 26 + ord(ch) - 64
return n
assert to_zero_free_ten(3020) == "2κ1κ" and to_zero_free_ten(10**6) == "99999κ"
assert column_label(27) == "AA" and column_label(16384) == "XFD"
for n in range(1, 10_000): # every encoding round-trips: the value is untouched
assert from_base(to_base(n, 3), 3) == n
assert from_zero_free_ten(to_zero_free_ten(n)) == n
assert column_number(column_label(n)) == n
assert (n % 3 == 0) == to_base(n, 3).endswith("0") # true in base 3 ...
assert any((n % 3 == 0) != str(n).endswith("0") for n in range(1, 100)) # ... false in base ten: a fact about notation
注意看最后两个断言。“能被 3 整除就末位是 0”在三进制下对所有数都成立,但最后一行展示了它在十进制下失效(3 能被 3 整除但末位是 3)。某个性质如果只在某种编码下成立,换个表示就失效,那它属于编码,不属于数本身。陈诗峰让孩子们判断一句话到底说的是“数”还是“写法”的排序实验,其实和“值不变、切换编码”的性质测试是同一个动作。在 b 进制下,数字和的捷径只对b − 1 (Weisstein, MathWorld, "Divisibility Tests"(中译:Weisstein,MathWorld,《整除检验》)的因数成立,所以我们熟悉的 3 和 9 的规则其实只是十进制的特性,不是三本身的特性。
同样的比特,不同的数:代码里最容易踩的坑
下面这三个例子,你可以直接在 Python 里复现。每一个都是“表示层的事实”渗进了“值层”。
- 一个字节,两种数。比特模式 11111011 走进酒吧,酒保问:“251 还是 −5?”它作为无符号字节读是 251,作为 8 位二进制补码读是 −5。比特完全一样,数值取决于约定的编码。只要两个系统对编码理解不一致,bug 就会跟着来。
- 0.1 + 0.2 得到 0.30000000000000004。二进制浮点数没法精确表示 0.1,就像十进制没法精确表示 1/3。在三进制里,1/3 就是 0.1。数值没问题,编码做了近似。
0.1 + 0.2 == 0.3这是编码层面的问题,所以数值代码都要用容差比较。 - 五个字符,一个值。字符 5、٥、५、Ⅴ 和 五 在 Unicode 字符数据库里都代表数值 5(Unicode Character Database (UnicodeData.txt) 和 Unihan (U+4E94))。Python 的
int("٥")和int("५")会返回 5,因为它们在 Unicode 里是十进制数字,而int("五")和int("Ⅴ")按设计 raise ValueError:int只认数字,不认数字符号。unicodedata.numeric("五")返回 5.0。每个解析器都要划定这条界限;问题在于你可能无意识地画错了。
供应链里的真实案例:产品编码“0042”被导入表格后,直接变成了数字 42,原本作为标识的前导零就这样丢了。于是某个仓库会误以为 0042(橡皮鸭)和 42(也是橡皮鸭,但供应商不同)是同一个产品,结果多下了一倍的订单。这个编码本来就是用数字字符串存储的标识符,从来不是数量。如果你能分清这个值到底属于哪一层,这种事故就不会发生。
一张带预测的“数字身体”地图
本系列借用了 Paul 的“play-engineering”笔记里的一个视角,先验证再假设:一个“身体”有灵魂、骨架、血肉和皮肤。对于数字系统来说:
- “灵魂”是数值本身;
- “骨架”是位值结构,也就是类型的骨架;
- “血肉”是数字操作的算法(进位、比较、解析);
- “皮肤”是字形和字节。
这个预测的意义在于:凡是能在编码变化后依然成立的,属于值本身;凡是换了编码就失效的,属于实现层。 加法属于灵魂,进位规则属于血肉:只有一个涉及 N 的规则(“到 N 就进一位”),所以换底只会改 N,本质的加法永远不变。代码里的原则就是“只在值层测试性质,把编码相关的事实藏在编码接口后面”。
但这张地图也有失效的时候:
- 你可以换骨架(变换进制)但保留灵魂,这可不是任何真正的身体能做到的。
- “离散”和“连续”其实是一个维度,不是分层。亚里士多德早就说过:「量要么是离散的,要么是连续的。」" (亚里士多德,《范畴篇》第六部分,E. M. Edghill 译(MIT Internet Classics Archive))。本系列讨论的全是离散量;测量(以及真正意义上的浮点数)留到下一季。
- 至于“精神”是否独立存在,这个问题还没有定论。柏拉图主义者认为,数是独立存在的抽象实体,不依赖于任何人的心智(Linnebo, Stanford Encyclopedia of Philosophy, "Platonism in the Philosophy of Mathematics"(中译:林内博,《斯坦福哲学百科全书》,《数学哲学中的柏拉图主义》));而名义主义者则认为,根本不存在抽象的“数”对象(Bueno, Stanford Encyclopedia of Philosophy, "Nominalism in the Philosophy of Mathematics"(中译:布埃诺,《斯坦福哲学百科全书》,《数学哲学中的名义主义》))。工程师大可不必纠结:只要接口好用就行。这本身也是一种哲学立场——一种实用主义。
为什么一个观念能贯穿四个年龄段
本系列押注于一个观点,通常追溯到 Jerome Bruner 的《教育过程》(1960):任何学科都能以某种智识上诚实的方式,在任何发展阶段被教给学生。我们是有意用自己的话转述的。原文我们没查到,所以不直接引用。
“诚实”的检验标准,就是同一个不变性测试。每一集都讲同一个内核,七岁学到的东西,二十岁时不用推翻重学。配对游戏就是休谟原则。「5 只是个名字」就是值/编码的区分。双手计数法就是进位规则。
本系列背后的“剧本”建立在你可以查证的研究之上:
- 工作记忆能同时容纳三到五个单元(Cowan (2001), "The magical number 4 in short-term memory", Behavioral and Brain Sciences 24(中译:Cowan(2001),《短时记忆的神奇数字4》,《行为与脑科学》24)).
- 对新手有帮助的支持,可能会妨碍专家(Kalyuga, Ayres, Chandler & Sweller (2003), “The Expertise Reversal Effect”, Educational Psychologist 38).
- 只有当你带着“要教别人”的心态去学,教学的效果才会真正显现(Kobayashi (2024), Educational Psychology Review (ERIC EJ1414484)).
再补充一个我自己的视角:我始终相信,真理、爱与美不是彼此独立的理想,而是同一个真实的三种面貌——在我的信仰里,甚至是同一个人的三重形象。在本系列中,这三者也可以当作一份纯工程师视角的核查清单:一堂课要真实(学生能亲自验证),要有爱(为具体的人而写,能在他们犯错时陪伴),还要有美感(有一个值得被展示的瞬间)。
- 只有真理没有爱,就是一份没人能读懂的正确证明。
- 只有爱没有真理,就是一场有趣但会误导人的游戏。
- 只有美却没有真理和爱,就是一张漂亮但讲错历史的信息图。
那些“只对了一半”的常见误区
- “0.1 存错了。” 实际上,它是严格按照规范,存成了最接近的二进制小数。让人意外的是编码方式本身,不是 bug。正确做法是用容差比较,或者直接用十进制类型。
- “前导零无所谓。” 对于数量来说没错,但对标识符就错了。比如“0042”作为产品编码,其实只是一个恰好全是数字的字符串。
- “26 进制的列标签就是字母。” 实际上,它是一个没有零的进位制数系,这也是为什么 Z 后面接的是 AA,而不是 BA。
现在,轮到你把这套思路教给别人
你的“反向教学”任务: 找一个 15 岁的孩子,和他一起推导“每个数在任意进制下只有唯一一种写法”的证明(见 第 3 集)。不要直接把步骤写给他看:你可以问他 352 除以 3 的余数是多少,接下来商怎么处理,让他自己体会到数字的选择其实没人规定。
目标: 这个 15 岁的孩子能独立写出 352 在 3 进制下的余数步骤(111001),并能用一句话说清楚为什么最后一位是唯一的。欢迎把孩子的年龄和结果留言在本文下。
为什么这件事重要:五个“为什么”
问题:数据管道把产品编码的前导零丢了,结果 0042 和 42 被合并,两个产品变成了一个。
- 为什么会合并? 某个加载器把“0042”当作数字 42 解析了。这两个字符串都指向同一个数值,结果导致标识符冲突。
- 为什么会被当成数字解析? 系统是根据字符的外观(全是数字)来推断类型的,而不是根据字段本身的实际含义。
- 为什么要靠外观来推断? 因为系统把“表示”(一串数字字符)误当成了“值”本身,这正是陈诗峰那一章专门提醒孩子们要避免的混淆。
- 为什么没有被及时发现? 没有任何测试是在“值”保持不变的前提下切换编码。如果有这样的测试,立刻就能暴露问题——就像转换器里用十进制对比时,某个“只在 3 进制成立”的规则在十进制下直接失效一样。
- 根本原因: 代码把依赖写在了表示层,而不是值层。修正方法就是本集的原则:先明确什么才是“值”(到底是数量还是标识符),所有和编码相关的细节都要封装在接口背后(Liskov & Zilles (1974), "Programming with abstract data types", ACM SIGPLAN Notices),并且要跨编码做不变性测试。
实操指南:本周就动手试试
- 先读一遍“准备教别人”那句话。
- 用一只手数到 31(2 分钟)。
- 写出那三个转换器和属性测试,然后和上面的版本对比(15 分钟)。
- 打开 Number Playground。输入 16384 和 1000000,把你的转换器和它的各行结果对一遍(包括表格列那一行:16384 就是 XFD),然后玩一把 N-or-R 游戏。
- 在你自己的代码里找一个依赖于表示的地方(比如字符串和数字、浮点数相等判断、只认数字的解析器),加一个不变性测试。
- 把第 3 集里的唯一性证明讲给一个 15 岁的孩子听。
怎么判断你做对了
- 你的转换器能通过上面所有断言。
- 你的代码里多了一个新属性测试,如果有表示层的事实泄漏到值层,这个测试就会挂。
- 你能用一句话分别解释:为什么 11111011 可以代表两个不同的数,为什么 0.1 + 0.2 在二进制浮点下不等于 0.3,以及为什么 Z 后面接着是 AA。
一周后请再自查一次。实验证明,测试的效果优于反复重读(Roediger & Karpicke, 2006, "Test-Enhanced Learning", Psychological Science 17,中译:测试强化学习优于重复阅读)。1 周 (Roediger & Karpicke (2006), "Test-Enhanced Learning", Psychological Science 17)。不过,测试无法检验:你在截止压力下还能否坚持这个习惯。CI 里的属性测试,才是真正能兜底的那道防线。
前瞻提醒:现在就设好“绊马索”
- 如果某个字段全是数字,先明确它是数量还是标识符。 在 schema 里写清楚你的判断,别等 loader 自作主张。
- 如果你用
==直接比较浮点数,停! 请用容差或 decimal 类型,并加一个 0.1 + 0.2 的测试。 - 如果你的解析器遇到 ٥ 或 五, 请有意识地做出选择:Python 的
int能识别像 ٥ 这样的 Unicode 十进制数字,但会拒绝 五 和 Ⅴ 这样的字符(这是有意为之)。写明你的系统接受哪些,并加测试。
本集的真、善、美
- 真: 每个论断都能跑通。转换器断言全通过,字节和浮点的例子在任何 Python 终端都能复现。
- 善: 这篇文章直接把一个 15 岁的学生交到你手上,每个常见误区都用“对的地方”来回应。
- 美: 一个想法,从五个苹果到 Excel 最后一列,每个年龄段都能成立,无需推翻前面的认知。(第 1 集里和石头牵手的蛋,现在成了全国卡车上的 0042 号纸箱。它的数量还是 1,只是名字一直在变。)
5S 自评表
| S | 主张 | 证据 |
|---|---|---|
| 简明 | 本文整体可读性达到 13 年级或以下。 | Flesch–Kincaid 可读性等级:8.4(分数 ≤ 13),经如下方式重新计算:node scripts/check-numbers-four-ages.mjs |
| 扎实 | 所有定义和数据均有出处,代码可直接运行。 | Excel 的16,384列(Microsoft); 休谟原则 |
| 犀利 | 位值记数法是一种对数长度、固定字母表的编码方式;写代码时,正确做法是只依赖数值本身。 | 对数长度 · 属性测试 |
| SMART | 本周实践:写好转换器 + 给自己的代码加一个不变性测试 + 教别人一次。 | 实操 · 演练场 |
| 惊喜 | 其实你的表格一直在用无零的 26 进制计数。 | XFD = 16,384 |
| 真 · 善 · 美 | 每个论断都能复现;有具体的人可以教;每个年龄段都只有一个核心想法。 | 真、善、美 |
参考文献
- 陈诗峰(Shifeng Chen),章节草稿《第一章 数和数的表示形式》(“Numbers and Their Representations”),2026年。原文章节阅读(PDF,中文)。本系列共同作者。
- Zalta, E. N. “Frege's Theorem and Foundations for Arithmetic.”(中译:弗雷格定理与算术基础。)斯坦福哲学百科全书. plato.stanford.edu
- Liskov, B. & Zilles, S. (1974). Programming with abstract data types.(中译:用抽象数据类型编程。)ACM SIGPLAN Notices. Crossref 10.1145/942572.807045
- Weisstein, E. W. “Digit.” MathWorld.(中译:“数字”,MathWorld。)mathworld.wolfram.com
- Weisstein, E. W. “Divisibility Tests.” MathWorld.(中译:“整除检验”,MathWorld。)mathworld.wolfram.com
- Microsoft. “Excel specifications and limits.”(中译:Excel 规格与限制。)support.microsoft.com
- Unicode 字符数据库,UnicodeData.txt。unicode.org
- 亚里士多德。《范畴篇》,第6部分,E. M. Edghill 译。classics.mit.edu
- Linnebo, Ø. “Platonism in the Philosophy of Mathematics.”(中译:数学哲学中的柏拉图主义。)斯坦福哲学百科全书. plato.stanford.edu
- Bueno, O. “Nominalism in the Philosophy of Mathematics.”Stanford Encyclopedia of Philosophy. plato.stanford.edu
- Cowan, N. (2001). The magical number 4 in short-term memory.Behavioral and Brain Sciences 24. doi:10.1017/S0140525X01003922
- Kalyuga, S., Ayres, P., Chandler, P. & Sweller, J. (2003). The expertise reversal effect.Educational Psychologist 38. doi:10.1207/S15326985EP3801_4
- Kobayashi, K. (2024). Interactive learning effects of preparing to teach and teaching.Educational Psychology Review. ERIC EJ1414484
- Roediger, H. L. & Karpicke, J. D. (2006). Test-enhanced learning.Psychological Science 17. doi:10.1111/j.1467-9280.2006.01693.x
- DeLamater, M. (1999), on Bruner's hypothesis.Education Week. edweek.org(secondary; the primary wording was not checked)