📖 名词溯源:补码为什么叫「补」? 「补码」的英文 two's complement,直译是「二的补数」。可以这样理解:在 8 位系统里,一个数和它的补码相加,正好等于 2⁸ = 256(也就是「进位后被丢掉的那个 1」)。所以 -5 的补码 11111011(251),可以看成是 5 的「补到 256 的那份」。名字里的「补」,就是「凑满」的意思。
🎯 本章学习目标
- 理解整数在内存里的二进制存储方式,以及「能存多大」由什么决定。
- 理解什么是「溢出」,以及为什么数存不下时会发生。
- 理解浮点数(小数)的存储方式,以及「精度误差」从哪来。
- 理解字符串(文字)如何通过「编码」变成 0/1 存进内存。
- 认识布尔类型,并理解类型之间如何转换。
- 会用位权展开手算二进制↔十进制互转,并用 bin()/int()/hex() 机器核对。
- 能解释并亲手演示 0.1+0.2 ≠ 0.3 的根因,掌握浮点数的「公差比较」正确姿势。
5.1整数:二进制里的数,能存多大
整数(int)是最「干净」的数据:一个整数,直接转成二进制,写进内存格子就行。
关键问题是:内存格子是有限的,一个整数能占几格?这就引出一个概念——字长(有多少个 bit)。格子越多,能表示的数就越大。
用 8 个 bit(1 字节)举例,能表示多少个不同的数?每个 bit 有 0/1 两种可能,8 个 bit 就是 2⁸ = 256 种组合。从 00000000(0)到 11111111(255),一共能表示 0 到 255 这 256 个非负整数。
那如果我要存一个比 255 还大的数,比如 1000,怎么办?答案:用更多 bit。16 个 bit 能表示 2¹⁶ = 65536 个数,32 个 bit 能表示约 42 亿个数。bit 越多,能装的数越大。
溢出:数太大,装不下了
现在想象一个只有 8 个 bit 的格子,里面存着最大的数 255(11111111)。如果这时候再加 1,会发生什么?
二进制里,11111111 + 1 会产生一个进位,但这个进位「没地方放」了——格子只有 8 位。结果,进位被丢掉,8 个 bit 全部变成 00000000,也就是 0!
255 + 1 变成了 0,这就是 溢出(overflow):结果超出了能表示的范围,导致数据「绕回」到了一个错误的值。
底层原理 溢出不是 bug,是存储空间有限的必然结果。就像汽车的里程表只有 5 位,从 99999 再走一公里,就跳回 00000。计算机的每个整数都有固定 bit 数,超过上限就「绕回」。好在 Python 的整数会自动扩容,几乎不会溢出——但很多底层语言(如 C)会,理解这一点能帮你避免未来的坑。
🧠 一句话记忆 bit 越多,数越大;数超上限,就会「绕回」(溢出)。8 位最多 255,16 位最多 65535。
位权展开:二进制与十进制的换算手册
图 5-1 里 10000101 算出 133,靠的是一张「权重表」。十进制数每往左一位就×10,因为每一位的权重是 10 的幂:个位 1、十位 10、百位 100……同理,二进制每一位的权重是 2 的幂:1、2、4、8、16、32、64、128——每往左一格,权重翻倍。把一串 0/1 读成数,就是「哪一位是 1,就把哪一位的权重加进来」。以 10000101 为例,先把 8 个权重从右到左写好(128、64、32、16、8、4、2、1),再逐位对齐:0 不贡献,三个 1 分别落在权重 128、4、1 的位置上,128 + 4 + 1 = 133。权重表一旦对齐,读二进制就只是加法。
再用 2 的幂感受一下「位」的威力:8 位能存 0~255(256 个数);16 位能存 0~65535(65536 个);32 位能存 0~4294967295(约 42.9 亿);64 位能存约 1844 亿亿。规律只有一条:每加一位,可存的数翻一倍——指数增长。所以工程设备的标定表常写「16 位足够」:把一个量程切成 65536 档,对绝大多数测量已绰绰有余。
📐 例题 1 二进制 ↔ 十进制互转:以 2026 和 101101 为例
(1) 把十进制 2026 转成二进制;(2) 把二进制 101101 转回十进制;(3) 用 Python 内建函数 bin()、int()、hex() 核对手算结果。
思路 十进制转二进制用「除 2 取余、逆序排列」;二进制转十进制用「位权展开」。算完用机器核对——手算是理解,机器是保证。
解 (1) 2026 除 2 取余,余数依次为:0、1、0、1、0、1、1、1、1、1、1(共 11 步,余到商为 0 为止),从最后一步往回读:11111101010。用位权展开自检:1024 + 512 + 256 + 128 + 64 + 32 + 8 + 2 = 2026 ✓。
(2) 101101 共 6 位,从右往左对齐权重 1、2、4、8、16、32;四个 1 分别落在权重 32、8、4、1 上,32 + 8 + 4 + 1 = 45。
(3) 机器核对:
print(bin(2026)) # 0b11111101010:手算 ✓
print(int("101101", 2)) # 45:手算 ✓
print(hex(2026)) # 0x7ea:十六进制——工程上地址、编号更常用的写法
顺带认识了十六进制:它把 4 个二进制位并成 1 个字符(0-9、a-f),是二进制的「速记法」。0b 前缀标记二进制、0x 标记十六进制,Python 都能直接识别——上一章讲 id() 时提过地址常用十六进制,现在你已经有能力亲手换算了。
答:(1) 2026 = 11111101010₂;(2) 101101₂ = 45;(3) bin/int/hex 输出与手算一致,另得 2026 的十六进制写法 0x7ea。 ⚠ 易错点:除 2 取余的余数要从下往上读(先算出的是最低位);位权展开前必须先把权重表从右往左对齐——错一位,全盘皆输。
回到「溢出」,Python 的底气来自一个设计决定:整数会自动扩容。C 语言里一个 32 位整数最多存到约 42.9 亿,再大就「绕回」;Python 的整数想存多大就自动加位,2 的 100 次方直接就能算:
print(2 ** 100)
# 1267650600228229401496703205376(31 位十进制数字,毫秒级算完)
但别误会:自动扩容不是「无限内存」,只是把「位不够就加位」交给了解释器处理——真到天文数字,位越多算得越慢,免费的午餐不存在,账单换了个寄法。底层语言坚持固定位数,是为了速度和空间的可预测性。这是工程世界里「精确的有限」与「灵活的无界」之间的经典取舍,两种选择各有主场。
5.2负数与补码:减法变加法
前面只讲了非负整数,那负数怎么存?最自然的想法是「留一个 bit 当符号位」——最高位是 0 表示正,是 1 表示负。但计算机实际用的是更巧妙的方法:补码(two's complement)。
为什么不用简单的「符号位」?因为计算机的加法器只会做加法。要让「减法」也能用加法电路完成,就必须有一种表示法,让「加上一个负数」等价于「减去它的绝对值」。
补码的规则(以 8 位为例):
- 正数的补码,就是它自己的二进制。
- 负数的补码:先写出它的绝对值,全部取反(0 变 1,1 变 0),再加 1。
举例:求 -5 的 8 位补码。
5 的二进制: 00000101
全部取反: 11111010
再加 1: 11111011 ← 这就是 -5 的补码
验证一下:5 + (-5) 用补码算:
00000101 (5)
+ 11111011 (-5)
= 00000000 (进位丢掉,结果是 0) ✓
完美!加法电路直接把两个补码相加,就自动得到了「5 + (-5) = 0」,不需要单独的减法电路。这就是补码的精妙之处——它让「减」变成了「加」。
底层原理 补码的核心价值是:用「加法」统一了「加减法」。CPU 里只要一套加法电路,就能同时做加法和减法(减法=加补码)。这极大简化了硬件设计。符号位不是「多出来的标志」,而是补码自然形成的结果——最高位为 1 恰好就是负数。
补码的另一种求法:模 256 减一下
「取反加一」是补码的标准求法,还有一个更直观的旁路:在 8 位的世界里,一个负数 x 的补码就是256 减去 |x|。因为 8 位一共有 256 个格子,从 0 「倒着数」|x| 格,跟「正着数」256−|x| 格落在同一格。比如 −36:256 − 36 = 220,而 220 的二进制正是 11011100——跟「取反加一」算出来的结果分毫不差。两种算法殊途同归:「取反加一」适合手算快,「模减」适合理解为什么。
顺着这条路还能看穿一个冷知识:8 位补码的范围为什么是 −128 ~ +127,负方向反而多一个?看两头就懂:最高位为 0 时,正数从 00000001 到 01111111,最大 +127;最高位为 1 时,负数从 10000001(−127)一路到 10000000——这个「全零加一个 1」没有对应的正数名额,就被定义为 −128。于是 256 个格子的分配是:正数 127 个、负数 128 个、零 1 个。零占了正方向的名额,负方向就多出一个——这不是 bug,是「一个零只能有一套编码」的必然。
📐 例题 2 用补码完整算一遍 85 − 36
(1) 求 −36 的 8 位补码;(2) 用「85 + (−36 的补码)」计算 85−36,写出二进制过程;(3) 用 Python 核对结果与「丢进位」的动作。
思路 三步走:求补码 → 二进制相加 → 丢掉第 9 位的进位。加出来的 8 位结果,就是答案的补码。
解 (1) 36 = 00100100;取反得 11011011;加一得 11011100(=220,与 256−36=220 一致)。
(2) 二进制相加:
0101 0101 (85)
+ 1101 1100 (220,即 −36 的补码)
-----------
1 0011 0001
↑
第 9 位进位:8 位寄存器装不下,被丢掉
→ 结果 0011 0001 = 32+16+1 = 49 ✓
(3) 机器核对(& 是按位与,这里用来把结果圈回 8 位;% 256 是「模 256」):
print(85 + 220) # 305:真实和
print((85 + 220) % 256) # 49:丢掉进位(模 256)后就是答案
print(85 - 36) # 49:与直接相减一致 ✓
整个减法过程 CPU 里自始至终只做了一件事:加法。减一个数=加它的补码,多出来的进位被硬件直接丢弃——上一节 note 里「一套加法电路统一加减法」的说法,在这里落到了实处。
答:(1) −36 的补码 = 11011100;(2) 85 + 220 = 305 = 1 0011 0001,丢进位得 0011 0001 = 49;(3) Python 核对一致。 ⚠ 易错点:丢进位不是「出错」,而是 8 位世界里所有运算的最后一步——位数一固定,进位天然无处安放。另注意 −128 这个端点:它只存在于负方向,01111111+1 会绕到 10000000 变 −128,这就是上一节「溢出绕回」在补码世界的具体长相。
补码在工程里最日常的用途,是把「偏差」塞进一个字节。比如记录一次测量与标准值的偏差:−3 ℃、0 ℃、+2 ℃……用一个无符号字节根本没法表达负号,而用补码视角看,同一个字节 11111101 既可以是 253(无符号读法),也可以是 −3(补码读法)——同一个字节,两本说明书。仪表通信协议里满篇都是这种「按补码解读」的约定:字节从没变过,变的是读法。
顺带把三个容易搅在一起的名词分开:原码是「最直觉」的写法——最高位当符号、其余照抄(+3 写 00000011,−3 写 10000011);反码是「取反」这一步的中间产物(−3 得 11111100);补码是「取反再加一」的最终形态(−3 得 11111101)。原码的毛病在于「零有两副面孔」(00000000 与 10000000 都表示零),加减电路还得先看符号位挑算法,硬件不胜其烦;补码把零统一成一副面孔,加减法一套电路通吃。三个名词的演化史,就是一部「为硬件省电路」的历史——离硬件越近的设计,越讲究「让电路少做事」。
辨析一个读完 5.1、5.2 后必然会冒出来的疑问:本章一直在算「8 位」「16 位」的格子,那 Python 里的整数到底是几位?答案有点意外:Python 的 int 没有固定位宽——需要多少位就自动加多少位,所以不存在本章意义上的「溢出绕回」,连 2 的 100 次方都照存不误。定宽整型(C、Java 等)走「格子思维」:省内存、贴硬件、能直接对应寄存器宽度,代价是你必须自己操心越界;Python 的 int 走「弹性思维」:用一点速度和内存做交换,换你永远不必在应用层计算「会不会溢出」。这不是谁对谁错,而是抽象层次的分工:贴近硬件的层用定宽格子省电路省内存,贴近人的层用弹性整数省心。将来解析设备报文、换算寄存器值时,你仍然需要 8 位补码那套定宽思维(本章近两千字全是为此铺路);写上层业务计算时,Python 替你兜底。两层思维都装进工具箱、知道何时切换,才算真正学透了本章。
print(2 ** 100)
# 1267650600228229401496703205376:位宽自动扩展,精确算出
# 同样的式子放进 64 位定宽整型(C/Java),早已溢出绕回
5.3浮点数:小数怎么存,误差从哪来
整数好存,但小数(浮点数,float)麻烦得多。因为小数里有个小数点,它的位置不是固定的——1.5、3.14、0.001,小数点到处跑。怎么把「小数点会移动的数」存进固定长度的 bit?
答案是 科学计数法的思路。就像我们可以把 123.45 写成 1.2345 × 10²,浮点数也把一个数拆成三部分来存:
| 部分 | 作用 | 类比 |
|---|---|---|
| 符号位(sign) | 正还是负,1 个 bit | 正负号 |
| 指数(exponent) | 小数点该「移」几位 | 10 的几次方 |
| 尾数(mantissa) | 有效数字 | 1.2345 里的 12345 |
浮点数的精度误差
这套表示法有个固有缺陷:尾数只有有限位,很多小数根本存不精确。
就像十进制里,1/3 = 0.33333……永远写不完;二进制里,很多十进制小数(比如 0.1)也永远写不完,只能截断近似。这就是为什么你在 Python 里会看到:
print(0.1 + 0.2)
# 输出:0.30000000000000004 而不是 0.3
不是 Python 算错了,而是0.1 和 0.2 在内存里本来就是「近似值」,两个近似值相加,误差就浮出来了。
核心思想 浮点数是近似表示,不是精确表示。它的误差来自「二进制无法精确表示某些小数」。这就像你用十进制写 1/3,无论写多少位 0.333……,都只是近似。涉及钱的程序(如银行)不会用浮点数,正是为了避开这个误差。
🧠 一句话记忆 小数是「近似值」,别指望 0.1 + 0.2 正好等于 0.3。要精确比较,就允许一点误差,或用专门的精确小数类型。
挖到根:0.1 的二进制为什么「写不完」
「二进制无法精确表示某些小数」这句话,值得亲手验一遍。十进制小数转二进制用「乘 2 取整法」:小数部分不断×2,每次取出整数位。拿 0.1 走一遍:0.1×2=0.2 取 0;0.2×2=0.4 取 0;0.4×2=0.8 取 0;0.8×2=1.6 取 1,剩 0.6;0.6×2=1.2 取 1,剩 0.2——注意,0.2 第二次出现了!从这里开始四步一循环,取出的数字是 0、0、1、1 的重复。所以 0.1 的二进制是:
0.0001100110011001100…… 「1100」这四拍无限循环,永远写不完
十进制里一目了然的 0.1,在二进制里竟是无限循环小数——就像十进制写 1/3 只能 0.333…… 一样。可 IEEE 754 的尾数只有 53 位,写不完就得截断,存进去的其实是「离 0.1 最近的那个 53 位二进制数」。用格式化输出能看到它的真身:format(0.1, '.20f') 得 0.10000000000000000555;同理,0.2 实际存的是 0.20000000000000001110,0.3 实际存的是 0.29999999999999998890。两个「略偏大」的数相加,结果自然比 0.3 略大——这就是 0.1 + 0.2 = 0.30000000000000004 的全过程。
📐 例题 3 0.1 + 0.2 ≠ 0.3 全链条实验与正确比较法
(1) 验证 0.1 + 0.2 == 0.3 的结果;(2) 把 0.1 累加 100 次(0.1×100 应等于 10),看误差如何积累;(3) 给出浮点数比较的正确姿势。
思路 第一问「验伤」,第二问「看误差累积」,第三问「开药方」——药方就是工程里的公差思维:不问「是否相等」,只问「差得够不够小」。
解 实验代码与实测输出:
print(0.1 + 0.2 == 0.3) # False:两者差了约 4×10⁻¹⁷
print(0.1 + 0.2) # 0.30000000000000004
total = 0
for i in range(100): # 把 0.1 加 100 次
total += 0.1
print(total) # 9.99999999999998:误差悄悄攒起来了
print(abs(total - 10) < 0.000000001) # True:公差判断——差值小于 1e-9 就算相等
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True:标准库的「近似相等」判断
三个层次全部实测:直接 == 判断为 False;100 次累加后误差攒到 10⁻¹⁴ 量级(每次加法只差一点点,架不住天天差);而公差判断和 math.isclose 都给出 True。误差无法消灭(二进制表示所限),但可以管理——把它圈进一个足够小的公差里。
答:(1) False;(2) 累加 100 次得 9.99999999999998;(3) 用 abs(a - b) < 1e-9 或 math.isclose(a, b) 比较,永远不要用 == 比浮点。 ⚠ 易错点:误差是「累积」的——单次 0.1+0.2 只差亿亿分之几,肉眼无害;但成千上万次累加后照样能到 10⁻¹⁴ 的量级,工程计算里这种漂移会让「理论上该是 10 的数」永远过不了 == 关。
把两个容易混的「装不下」分开:溢出是整数的故事——格子装不下整数个,直接绕回,是「量」的问题;精度误差是小数的故事——格子装得下,但装的是近似值,是「准」的问题。顺带一提,浮点数也有自己的溢出:超过约 1.8×10³⁰⁸ 就存不下了,Python 会显示 inf(无穷大)——这是指数位装不下的「绕回」版。两类问题、两套药方,别开错方子。
浮点比较用公差,这不是程序员的妥协,而是工程本来的样子:机加工的图纸从不写「直径 = 10」,写的是「直径 10 ± 0.01」——在公差带内即为合格。测量、装配、验收全部按公差思维运转。浮点数的「绝对相等」本来就只存在于数学里,真实世界从来都是「够近就算同」。
5.4字符串:文字怎么变成 0/1
文字(字符串,str)是计算机里最「不像数字」的数据。它怎么存进只有 0/1 的内存?靠的是 编码(encoding)——给每个字符约定一个数字编号,存的时候存编号。
最著名的编码是 ASCII:它给英文字母、数字、符号各分配一个 0–127 的数字。比如大写 A 是 65,小写 a 是 97,空格是 32。
字符 'A' → 编号 65 → 二进制 01000001
但 ASCII 只有 128 个位置,装不下中文、日文等几万个字符。于是有了 Unicode:它给全世界几乎每个字符都分配了一个唯一编号(叫「码点」),中文、表情符号都有份。Python 的字符串默认就用 Unicode。
核心思想 字符串的存储本质,是「字符 → 编号 → 二进制」的映射。编码表(ASCII、Unicode)就是这个映射的「字典」。计算机里没有「字」,只有「数字」——文字不过是按约定解释的数字。
📖 名词溯源:ASCII 是什么的缩写? ASCII 全称 American Standard Code for Information Interchange(美国信息交换标准代码)。它诞生于 20 世纪 60 年代,最初是为了让不同厂家的设备能「用同一种编号」交换文字。今天它依然是所有编码体系的地基——Unicode 的前 128 个编号,和 ASCII 完全一致。
三兄弟辨析:ASCII、Unicode、UTF-8
三个名字新手最容易混成一团,其实分工清清楚楚。ASCII是最早的编号表,只用 1 字节给 128 个字符编号(英文字母、数字、常用符号);Unicode是当今的「大编号表」,立志给全世界的每个字符一个编号——汉字「适」编号 36866,「航」编号 33322,Emoji、藏文、古文统统有份;而 UTF-8不是编号表,是存储格式——它规定「怎么把 Unicode 编号装进字节」。三兄弟的关系一句话:Unicode 给编号,UTF-8 定存法,ASCII 是 UTF-8 的前 128 项。
UTF-8 最聪明的地方是「变长」:编号小的字符用 1 字节装(英文字母跟 ASCII 一模一样),编号大的用多字节装——UTF-8 的两字节形态最多只装到编号 2047,汉字「适」的编号 36866 超了这个界,就升格用 3 个字节装:e9 80 82。变长设计两头讨好:纯英文的文本跟 ASCII 完全兼容(当年堆积如山的英文数据不用改一个字节),中文等多字节字符也装得下(代价是每个汉字 3 字节)。所以「适航」两个字用 UTF-8 存,共 6 字节;而字符串 "Hi" 只占 2 字节——变长,省得恰到好处。
顺藤摸瓜,乱码的本质也清楚了:写进字节时用一张表,读出来时用了另一张表,双方言不由衷。字节没坏,坏的是「约定的错位」。Python 里字符串(str)一律按 Unicode 记账,字节的原始形态叫 bytes(如 b'Hi'),两者通过 .encode()(文字→字节)和 .decode()(字节→文字)互相转换——第 11 章读写文件时,「打开文件要用哪个编码」会成为真问题,今天先把概念挂好钩。
📐 例题 4 编码表查询实验:ord、chr、encode 三件套
(1) 查 "A"、"适"、"航" 的编号;(2) 把编号 33322 翻译回字符;(3) 统计 "Hi" 和 "适航" 用 UTF-8 存各占几个字节,并打印「适」的字节内容。
思路 ord() 查「字符→编号」,chr() 反查「编号→字符」,.encode('utf-8') 看实际存储的字节,len() 数字节数——四件工具把「字符 → 编号 → 字节」的链条全部打通。
解 实验代码与实测输出:
print(ord("A")) # 65:ASCII 区段,1 字节就能装
print(ord("适")) # 36866
print(ord("航")) # 33322
print(chr(33322)) # 航:编号反查字符,ord/chr 互为逆操作
b1 = "Hi".encode("utf-8")
print(len(b1)) # 2:两个英文字符各占 1 字节
b2 = "适航".encode("utf-8")
print(len(b2)) # 6:两个汉字各占 3 字节
print(b2.hex()) # e98082e888aa:适=e98082,航=e888aa
链条完整闭合:A=65(1 字节)、适=36866(3 字节 e9 80 82)、航=33322(3 字节 e8 88 aa)。注意 ord("适") 给的是 Unicode 编号(36866),而 .hex() 显示的是 UTF-8 存储字节(e9 80 82)——编号与存法是两回事,这正是本节辨析的活例子。
答:(1) 65、36866、33322;(2) chr(33322) = '航';(3) "Hi" 占 2 字节、"适航" 占 6 字节,适 的字节为 e9 80 82。 ⚠ 易错点:别把 Unicode 编号(36866)与 UTF-8 字节(e9 80 82)混为一谈——前者是「户口号」,后者是「装箱方式」;len(字符串) 数的是字符数,len(编码后的字节串) 数的才是字节数,两者对中文来说相差 3 倍。
最后拆掉一个常见混淆:编码不等于加密。编码(ASCII、UTF-8)是公开的约定——全世界共用一张表,谁都能查、谁都能还原,目的是「统一记账」;加密是保密的变换——目的恰恰是不让外人读懂,需要密钥才能还原。把文档「转成 UTF-8」任何机器都能正常打开;把文档「加密」之后,没有密钥谁也打不开。一个是修路,一个是上锁。
5.5布尔类型与类型转换
还有一类「只有两个值」的数据,叫 布尔类型(bool):它只有 True(真)和 False(假)两个取值。
is_adult = True
has_license = False
布尔值看起来简单,却是整个「判断」「分支」「循环」的基础——第 7、8 章的所有逻辑,都建立在「一个条件是 True 还是 False」之上。它本质上就是第 1 章说的「二选一」,是计算机世界里最小的、最根本的「选择」。
类型转换:数据「改头换面」
不同类型之间,有时需要互相转换。Python 提供了几个转换函数:
int("42") # 字符串 "42" → 整数 42
str(42) # 整数 42 → 字符串 "42"
float("3.5") # 字符串 "3.5" → 浮点数 3.5
bool(0) # 整数 0 → False(0 为假,非 0 为真)
转换的意义在于:有些数据「长得像数字,其实是文字」。比如从文件里读到的 "42" 是字符串,不能直接做加减,必须先 int("42") 转成整数才能算。类型转换,就是给数据「换一张解读说明书」。
| 类型 | 关键字 | 例子 | 内存里存的是 |
|---|---|---|---|
| 整数 | int | 42 | 二进制补码 |
| 浮点数 | float | 3.14 | 符号+指数+尾数 |
| 字符串 | str | "hi" | 字符编码序列 |
| 布尔 | bool | True | 本质是 1 或 0 |
🧠 一句话记忆 数据在内存里全是 0/1,「类型」决定了怎么解读它。转换类型,就是换一张解读说明书。
布尔的身世:整数家族的「特例成员」
True 和 False 看着高高在上,其实在 Python 里有个低调的出身:布尔是整数家族的特例——True 就是 1,False 就是 0。可以直接验证:True == 1 得 True,False == 0 得 True,int(True) 得 1。这个设计源自计算机的老传统:电路里本来就只有「有电(1)/没电(0)」两种状态,真假和一零天然是同一件事的两种读法。
更实用的规则是「隐式真假」:Python 在需要真假的地方,会把任何值自动按规则读成真或假——空与零为假,其余为真。bool(0) 是 False、bool("") 是 False(空文字)、bool(3) 是 True、bool("hello") 是 True。这里埋着新手必踩的坑:bool("False") 竟然是 True——因为 "False" 是「非空字符串」,非空即真!字符串里写的字是「假」,但字符串本身「有内容」,所以是真。文字内容与真假身份,是两个层面的事,第 7 章写条件判断时这个坑还会再出现一次,先在此立牌提醒。
📐 例题 5 类型转换陷阱五连测
先预测、再运行核对:(1) int("42");(2) int(3.9);(3) int("3.5");(4) bool("False");(5) str(42) + "1"。
思路 int() 挑食——字符串里必须是「长得像整数」的才能转;数字转 int 是「截断」不是四舍五入;bool 按非空即真;str() 万能、转完是文字。
解 实测输出:
print(int("42")) # 42:"42" 长得像整数,转成功
print(int(3.9)) # 3:小数直接砍掉(向零取整),不是四舍五入!
print(int("3.5")) # ValueError: invalid literal for int() with base 10: '3.5'
print(bool("False")) # True:非空字符串一律为真——字面是 False 也拦不住
print(str(42) + "1") # 421:42 先变成文字 "42",再和 "1" 拼接
五问五个知识点:字符串转数字要求「长得像」;浮点转整数是截断(3.9 → 3,-3.9 → -3,都朝零靠);带小数点的字符串 int() 直接拒绝(想转得先用 float());布尔的判定看「空不空」不看「内容是什么」;str() 是万能桥,过桥之后只能做文字操作(拼接)。
答:42;3;报 ValueError;True;'421'。 ⚠ 易错点:最难接受的是第 2、3 问的组合——int(3.9) 能转(截断得 3),int("3.5") 却报错:Python 的原则是「数字截断可以,含小数点的文字不认」。要正确舍入用 round(),要转带小数的字符串用 float("3.5")。
为什么转换如此重要?因为从外部进来的数据几乎全是字符串:从配置文件读到的 "42"、从仪表读数报文里解出的 "3.5"、用户在键盘上敲的一切——到达程序时都是文字。要参与计算,就必须先转换。这一步没做或做错,是新手程序「一跑就报错」的头号原因(下一章你会看到 "5" + 3 的报错现场)。Python 的态度是「显式」:从不偷偷帮你转类型,类型不对就当场报错、当场指出——错得早、错得响,好过错得晚、错得静。
最后拆一对双胞胎:int(3.9) 是截断(3.9 → 3,直接砍掉小数);round(3.9) 才是舍入(3.9 → 4)。要四舍五入用 round(),要「抹掉零头」用 int()。round 还有个冷知识:Python 的 round 用「银行家舍入」,遇到 .5 时取偶数——round(2.5) 得 2、round(3.5) 得 4。这不是 bug,是财会传统(长期统计下偶数舍入更平衡),但足以让你在边界值上惊掉下巴,记住「.5 向偶数靠」即可。
本章从整数、小数、文字一路讲到布尔与转换,收尾时把地图摊开、看看下一段路:数据有了「类型」,下一章的运算才有「规则」。类型的分量比你现在感觉的重——同一个加号,见到整数做算术、见到字符串做拼接;同一个比较,见到数字比大小、见到字符串按编码顺序排先后。运算符从不凭空工作,它总是先问一句「你是什么类型」,再决定执行哪套动作。所以第 6 章里任何让你想不通的运算结果,答案多半要回到本章来找——「0.1 + 0.2 为什么不等于 0.3」,你已经比大多数人先看透了一层。
本章小结
- 整数用二进制补码存储,能存多大由 bit 数决定;超上限会溢出(绕回)。
- 补码让「减法变成加法」,硬件只需一套加法电路。
- 浮点数用「符号+指数+尾数」近似存储,所以存在精度误差(如 0.1+0.2 ≠ 0.3)。
- 字符串靠编码(ASCII/Unicode)把字符映射成数字再存。
- 布尔只有 True/False;不同类型可通过函数转换。
- 位权展开是进制互转的手算依据:每往左一格权重翻倍(1、2、4、8…);bin()/int() 是核对工具,hex() 给出十六进制速记。
- 8 位补码范围 −128 ~ +127:零占一个名额,负方向就多一个;求补码可「取反加一」,也可「模减」——256−36=220=11011100。
- 0.1 的二进制是 0.000110011…(1100 循环),53 位尾数装不下只能近似——这就是浮点误差的根;比较浮点永远用公差:abs(a−b)<1e-9 或 math.isclose()。
- 编码三兄弟分工:Unicode 给编号、UTF-8 定存法(变长:英文 1 字节、汉字 3 字节)、ASCII 是前 128 项;乱码=读写双方用错约定表。
读完本章,你应该能……
- 说出 8 位二进制能表示多少个非负整数,以及为什么。
- 解释「溢出」是什么,并类比生活中的「里程表绕回」。
- 手算 -5 的 8 位补码,并验证 5 + (-5) = 0。
- 解释为什么 0.1 + 0.2 在 Python 里不等于 0.3。
- 说明字符串「编码」的本质,并说出 ASCII 和 Unicode 的关系。
- 手算 2026 的二进制(或 101101 的十进制),再用 bin()/int() 验证。
- 用补码完整算一遍 85−36,说清「丢进位」发生在哪一步。
- 写出 0.1+0.2≠0.3 的演示代码和公差比较代码(abs / math.isclose)。
- 说出「适航」用 UTF-8 存占几个字节、字母 A 占几个,并解释原因。
- 逐个预测 int("3.5")、int(3.9)、bool("False") 的结果并运行核对。
思考题
1. 8 位二进制能表示 0 到 255,那 16 位能表示 0 到多少?怎么算?
提示:2¹⁶ = 65536 种组合,所以是 0 到 65535。
2. 为什么说「补码让减法变成了加法」?它对硬件设计有什么好处?
提示:加一个负数 = 加它的补码,所以 CPU 只需加法电路即可同时做加减,省掉减法电路。
3. 银行系统为什么不用浮点数来存钱?
提示:浮点数有精度误差,0.1 都存不精确;存钱需要绝对精确,所以用专门的精确小数或整数(以「分」为单位)。
4. 字符串 "A" 存在内存里的,到底是「字母 A」还是「数字」?为什么?
提示:是数字(编号 65)。计算机只认 0/1,字符只是按编码表解释的数字。
5. 8 位补码的取值范围为什么是 −128 ~ +127,负方向反而多一个?
提示:256 个格子里,最高位为 0 的正数名额只有 127 个(00000001~01111111),零又占去一个名额(00000000),而 10000000 这个「没有正数对手」的编码被定义为 −128——于是负数 128 个、正数 127 个、零 1 个。
6. round(2.5) 在 Python 里等于几?为什么不是 3?
提示:等于 2。Python 的 round 采用「银行家舍入」,遇到 .5 向偶数靠:round(2.5)=2、round(3.5)=4。长期统计下这种舍入方式两边更平衡,是财务领域的老传统。
7. 「适航」两个字用 UTF-8 存占几个字节?字母 A 呢?为什么差这么多?
提示:6 字节 vs 1 字节。UTF-8 是变长编码:编号小于 128 的字符(英文字母、数字)1 字节搞定;汉字编号大,需要 3 字节。变长的意义是既兼容海量存量英文数据,又装得下全世界的文字。
8. int(3.9) 和 round(3.9) 各得几?int("3.5") 呢?三者的区别说明什么?
提示:3、4、报错。int() 对数字是截断(砍掉小数),round() 才是舍入;而 int() 对字符串只认「长得像整数」的内容,"3.5" 直接 ValueError。数字转换和文字转换是两条不同的规则,别混着记。
9. len("Hi") 和 len("Hi".encode("utf-8")) 各等于几?换成 len("适航") 和 len("适航".encode("utf-8")) 呢?
提示:2 和 2;2 和 6。len() 数字符串时数的是「字符」个数,数字节串(encode 之后)时数的是「字节」个数。英文 1 字符=1 字节,两者相等;中文 1 字符=3 字节,差 3 倍。判断「这段文本占多少存储」要看字节数,判断「有几个字」要看字符数——两个计数单位,别拿错尺子。
10. 无符号读法下的 8 位 255,改用补码(有符号)读法是多少?这说明什么?
提示:255 = 11111111,补码读法是 −1。同一个字节、两本说明书——通信协议里每个字节都必须先约定「按哪本读」,否则双方各执一词。字节从没变过,变的是读法。
11. 2 ** 100 在 Python 里能算出精确值吗?放进 C/Java 的 64 位定宽整数里呢?为什么?
提示:Python int 位宽自动扩展,精确算出 1267650600228229401496703205376;定宽整数只有 64 个格子,装不下 2 的 100 次方,只能溢出绕回。定宽省内存贴硬件、弹性省心贴应用——是分工,不是优劣。