AI 技术博客
返回首页
Python 基础 · 15 分钟阅读

数字类型:int / float / complex 的完整画像

## 引言 上一篇我们确立了核心心智模型:变量是名字,对象在堆上,赋值是绑定。现在把目光投向这三类最基础的数字对象:int、float、complex。它们是第二章「变量、类型与运算符」的第二种类型,也是后续所有数值计算的原材料。 本篇要回答三个问题:为什么 `2 ** 100` 在 Python 里不会溢出、在其他语言里会?为什么 `1.1 + 2.2` 不等于 3.3,而 `1.1 + 2.2` 与 `3.3` 的差又恰好是某个固定的小数?复数(complex)类型真的是「锦上添花」吗——不是,信号处理、FFT、电气工程里它无处不在。读完本篇,你将能用 sys.float_info 自行核验浮点数的精度边界,理解 int 与 float 转换时截断的方向,并掌握比较浮点数的正确工具 math.isclose。 ## 概念与原理 ### int:二进制补码意义上的「无限长」 很多语言里整数是固定 32 位或 64 位的,溢出后回绕甚至报错。Python 的 int 是**任意精度整数(arbitrary precision integer)**:内部按「数组 + 进制基」分段存储,CPython 在 64 位平台上以 2³⁰ 为一“位”(digit),在 32 位平台上以 2¹⁵ 为一“位”,需要多少段就分配多少段,多大的整数都能精确表示,唯一的"上限"是内存。代价是性能:Python 整数运算比原生寄存器整数慢一个数量级,因为每次运算都要走对象分配与逐段进位。这也是为什么大规模数值计算要用第三方的 NumPy(课程 06 的专题)。 int 还有几个工程常用特性:字面量里可用下划线分隔大数(1_000_000);支持二进制、八进制、十六进制字面量 0b/0o/0x;内置 bin()、oct()、hex() 与带进制参数的 int(x, base) 做进制转换。注意 `int("ff", 16)` 里的第二个参数是「进制基」而非常规的第三个参数,这是初学者最容易写错签名的内置函数之一。 ### float:IEEE 754 双精度,53 位有效二进制位 float 在 CPython 里就是 C 语言的 double,遵循 IEEE 754 双精度标准:1 位符号 + 11 位阶码 + 52 位尾数,加上隐含的前导 1,共 53 位有效二进制位,十进制稳定精度约 15-17 位(sys.float_info.dig 给出 15,即可靠往返的十进制位数)。任何十进制小数转成二进制小数都可能无限循环(就像 1/3 在十进制里无限循环),只能截取 53 位凑合——**这就是浮点误差的根源**,不是 Python 的 bug,是二进制表示的物理事实。 「15 位可靠精度」这个数字本身可以推出来:IEEE 754 双精度按 (-1)符号 × 1.f(二进制尾数) × 2^(阶码-1023) 组织数值,有效二进制位 53,而十进制有效位数约等于 53 × log10(2) ≈ 15.95——这就是 sys.float_info.dig 为 15、实际可靠位数在 15 到 17 之间浮动的数学根源。二进制里只有 1/2、1/4、1/8 这类 2 的负整数次幂是有限小数,其余十进制小数展开成二进制几乎都是无限循环:0.1 在内存里存的其实是 0.1000000000000000055511151231257827… 这个无穷展开的截断值。于是误差是**确定性的**:0.1 + 0.2 与 0.3 的差恒为 5.5511e-17(本机用 Decimal 精确核对),每次运行都是同一个数,而不是随机噪声。 由此可以引出三个推论。第一,`sys.float_info` 给出了这台机器上 float 的全部边界:最大有限值 max ≈ 1.8e308,最小正规数 min ≈ 2.2e-308,机器精度 epsilon ≈ 2.2e-16(1.0 与下一个可表示浮点数之间的距离)。第二,`0.1 + 0.2` 不等于 0.3 不是个例,任何「看起来干净」的十进制小数的运算都可能不精确,所以比较浮点数要比较「差的绝对值是否足够小」,标准库提供了 math.isclose。第三,repr(float) 从 Python 3.1 起保证「最短往返表示」:打印一个浮点数,它显示的那串字符转换回 float 后一定得到原值——这是让调试输出不吓人的背后机制。 ### complex:一个直角坐标对 complex 就是两个 float 组成的「实部 + 虚部」对,写作 `a + bj`(j 是电工传统的虚数单位记号)。底层没有任何魔法:加减乘除就是二元组的四则运算,乘法展开为 (a+bi)(c+di)=(ac-bd)+(ad+bc)i。abs() 返回模(到原点的欧氏距离),z.real 与 z.imag 分别取出实部虚部。在只做数据处理的项目里 complex 很少见,但在 FFT、滤波器设计、量子计算模拟里它是标配:FFT 的输出天然是一串复数,滤波器系数是复数,量子概率幅本身就是复数。Python 把复数做成内置类型、并配套提供 cmath 标准库(复数的超越函数与极坐标转换),意味着写 DFT 实验代码不需要引入任何第三方库——课程 06 的数值计算专题会用上这套能力。 ### 溢出、无穷与 NaN:浮点世界的边界 浮点运算除了有限值,还引入了两个特殊量:正负无穷(inf / -inf)与 NaN(非数,Not a Number)。1e308 是最大有限值,乘以 10 就溢出为 inf;inf 除以自身、0 乘以 inf 得到 NaN。NaN 有一个著名性质:**它不等于自身**,`nan == nan` 为 False,用 == 甚至 is 判断都会得到「相等不存在」的结论,判断 NaN 必须用 math.isnan,判断值是「有限的常规数」可以用 math.isfinite。传感器数据掉线、数值算法迭代发散时,这些特殊量会真实出现在数据流里,过滤脏数据的代码因此常写 math.isfinite(x) 而不是 x != inf。 ### 除法与取模的家族 `/` 恒返回 float(即使整除),`//` 返回向下取整(floor)的整数,`%` 是这种 floor 除法意义下的余数,divmod() 一次给出两者。向下取整意味着 `-7 // 3 == -3`(向负无穷方向,而不是向零截断),对应 `-7 % 3 == 2`。这个取模语义在运算符章节还会系统展开,本篇只需知道「% 的结果符号总是与除数一致」。 ## 操作与实现 先看 int 的全貌:任意精度、进制与分隔符。另外,float 字面量支持科学计数法:1.5e3 表示 1500.0,4.2e-7 表示 0.00000042,读写极大极小的数远比把零数清楚可靠;float("1.5e3") 也能直接解析科学计数法,而 int() 不接受小数点或 e——这个能力差异是文本解析代码里最常见的分叉点,第五节会展开。 ```python tenth = 1_000_000_000 print(tenth) # 1000000000:下划线仅作视觉分隔 print(bin(255)) # 0b11111111 print(oct(255)) # 0o377 print(hex(255)) # 0xff print(int("ff", 16)) # 255:字符串按基 16 解析 big = 2 ** 100 print(big) # 1267650600228229401496703205376:任意精度,不溢出 print(len(str(big))) # 31:位数为 31,实打实的大整数 ``` 再看向浮点误差的现场。下面这些输出全部来自本机 Python 3.12 的真实运行,你没有看错,就是这些数字。 ```python import sys print(sys.float_info) # 机器浮点边界全表 print(sys.float_info.dig) # 15:十进制可靠有效位数 print(sys.float_info.epsilon) # 2.220446049250313e-16:机器精度 print(0.1 + 0.2) # 0.30000000000000004 print(1.1 + 2.2) # 3.3000000000000003 print(1.1 + 2.2 == 3.3) # False:精确相等判断失败 ``` sys.float_info 是一个由 namedtuple 派生的小对象,max、max_10_exp、min、min_10_exp、dig、mant_dig、epsilon 等字段的含义在标准库文档里有完整说明,只要记住关键词「53 位有效位 / 15 位十进制 / epsilon 2.2e-16」就能应付 99% 的场合。 比较浮点数的正确姿势是 math.isclose:它默认要求两数差的绝对值不超过 rel_tol\*max(|a|,|b|) 加上 abs_tol,两个容差都可以自己调。 ```python from math import isclose, pi print(isclose(1.1 + 2.2, 3.3)) # True:差约 4.4e-16,远小于默认容差 print(isclose(pi, 22 / 7)) # False:相对误差约 4e-4,超出默认 rel_tol=1e-9 print(isclose(pi, 22 / 7, rel_tol=1e-3)) # True:放宽相对容差到千分之一 print(round(2.5)) # 2:银行家舍入(就近取偶) print(round(3.5)) # 4:而不是常规的四舍五入 print(round(2.675, 2)) # 2.67:2.675 的二进制近似略小于真值 ``` round() 遵循的是「银行家舍入」(round half to even):恰好落在两个候选值正中间时,取偶数的那个。2.5 在 2 和 3 中间取 2,3.5 在 3 和 4 中间取 4。而 round(2.675, 2) 输出 2.67 则更精彩:2.675 在内存里的值其实是 2.6749999999999998……,小于一半,于是被舍成 2.67——这恰恰是浮点误差的又一个实例,值得反复体会。 复数的操作与工程示例结合起来看。 ```python z = 1 + 2j # 字面量:实部 1,虚部 2 print(z.real, z.imag) # 1.0 2.0:取出的都是 float print(abs(z)) # 2.23606797749979:模 = sqrt(1²+2²) print(z * (3 - 4j)) # (11+2j):(ac-bd)+(ad+bc)i print(complex(0, 1) ** 2) # (-1+0j):虚数单位平方等于 -1,字面验证 ``` 最后是 int 与 float 转换的截断语义与除法的 floor 家族。工程上最常见的数值场景是「文本转数字」:从配置文件、命令行参数、网页表单读到的全是字符串。int() 与 float() 是解析入口,但宽容度截然不同:int("3") 成功、int("3.0") 报错、float("3") 与 float("3.0") 都成功——选哪个入口取决于输入格式是否可控,这也是后续章节做数据清洗时反复出现的判断。 ```python print(int(3.99)) # 3:向零截断,不是四舍五入 print(int(-3.99)) # -3:同样是向零 try: int("3.14") except ValueError as e: print(type(e).__name__, ":", e) # ValueError: invalid literal ... print(int("3") + 4) # 7:字符串转 int 才能参与算术 print(7 / 2) # 3.5:/ 恒返回 float print(7 // 2) # 3:floor 除法 print(-7 // 3) # -3:向负无穷取整 print(-7 % 3) # 2:余数符号与除数一致 print(divmod(-7, 3)) # (-3, 2):商与余数一次给出 ``` ## 易错点与陷阱 ### 陷阱一:直接用 == 比较浮点数 看到 `1.1 + 2.2 == 3.3` 为 False 就慌了?这是二进制表示的正常现象。正确做法是 math.isclose;如果两个数来自同一份数值推导(比如同一次迭代的中途结果),还可以用它们之间相减的绝对值与阈值比较。特别注意:isclose 在两边都接近 0 时行为不同(rel_tol 乘 0 还是 0),此时要显式给 abs_tol,比如 `isclose(x, 0.0, abs_tol=1e-9)`。 ### 陷阱二:金额、计费数据用 float 累加 0.1 元累加 10 次很可能得到 0.9999999999999999。金融场景应该用标准库的 decimal.Decimal(以十进制十进制定点表示,可以精确到分),或者全部换算成整数「分」计算。这个知识点在标准库章节会系统介绍,现在记住结论:float 适合科学计算与传感器数据,不适合「必须精确到分」的账务数据。 ### 陷阱三:int("3.14") 与 int(3.14) 分不清 两个写法一个报 ValueError,一个安静截断。`int("3.14")` 是字符串解析,要求字符串整体是合法整数;`int(3.14)` 是浮点转整数,直接向零截断小数部分——想四舍五入要自己写 round(int(...)) 的组合或加 0.5 取整(负数时注意方向)。工程上最常见的错误是从配置文件读回数字时没做类型转换:读出来是字符串 "2500",直接参与乘法得到 "25002500" 而不是 25000000,排查半天。 ### 陷阱四:误以为 round() 是四舍五入 银行家舍入让 `round(2.5) == 2`,让「四舍五入」教条的读者猝不及防。更麻烦的是 round(2.675, 2) 这类浮点意外。需要严格的「四舍五入到 n 位小数」时,工程上常用 Decimal 的 quantize 或第三方的 decimal 上下文控制,而不是原生 round()。 ### 陷阱五:拿浮点当循环计数器 `x = 0.0` 循环累加 0.1 十次,结果是 0.9999999999999999(本机实测),而 x < 1.0 的循环条件在第十次时恰好成立、第十一次后永不成立——累计误差让退出条件变得不可预期。次数类计数请一律用整数或 range(第三章的主题),浮点只承担「度量」职责:量温度、算比例、测时间,而不数次数。 ## 小结 int 是任意精度整数,靠分段存储换来了不溢出的能力,代价是比原生整数慢;float 是 IEEE 754 双精度,53 位有效二进制位决定了它无法精确表示大部分十进制小数,误差由此而来,比较要靠 math.isclose;complex 是一对 float,承载复平面上的点。round 走银行家舍入,int() 对浮点向零截断,处理「分」级精度时要主动选择 Decimal。下一篇进入布尔值——True 与 False 背后的真值测试逻辑,你会发现它和本篇的数字类型有着惊人的血缘关系(剧透:True 其实就是整数 1 的子类)。 ## 练习与思考题 1. 用 sys.float_info 验证:epsilon 加上 1.0 之后是否大于 1.0?如果给 1.0 加上 epsilon 的一半再比较结果是什么?解释原因。 2. 写出 `int("0x1f", 16)`、`int("101", 2)`、`int("371", 8)` 的值并运行验证,然后说明 int(x, base) 对字符串前缀 0x/0o/0b 的宽容行为。 3. 为什么 `round(2.675, 2)` 得到 2.67 而不是 2.68?用 `format(2.675, '.20f')` 观察 2.675 的真实二进制展开再说出结论。 4. 思考题:`0.1 + 0.2 == 0.3` 为 False,但 `0.5 + 0.25 == 0.75` 为 True。同样是十进制小数,为什么后者碰巧精确?提示:0.5 与 0.25 都是 2 的负整数次幂,它们在二进制下是有限小数。