Python 基础体系 · 第 6/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 数值类型:int、float、complex、bool 与运算边界

Python 3.14 内置了三种主要数值类型:intfloatcomplexbool 并列看似是逻辑类型,但在类型层次上是 int 的子类。理解它们不能只停留在“整数、浮点数、复数、布尔值”的分类上,还必须继续追踪三个问题:

  1. 一个字面量最终创建了什么类型的对象;
  2. 不同数值类型混合运算时如何转换;
  3. 运算结果在哪些地方不再满足直觉中的数学规则。

Python 的数值语义同时受到数学模型、对象类型和具体浮点表示的影响。int 可以保持任意精度,float 通常受机器浮点格式限制,complex 的实部和虚部又分别依赖浮点数;bool 虽然只有两个值,却能够参与整数运算。(docs.python.org)


1. 先建立类型模型:值、类型与数值协议

在 Python 中,变量本身没有固定的“数值槽位类型”。变量只是绑定到对象:

x = 10
print(type(x))  # <class 'int'>

x = 10.0
print(type(x))  # <class 'float'>

x = 10 + 2j
print(type(x))  # <class 'complex'>

因此,x 并不是先声明为整数再“变成”浮点数;而是先绑定整数对象,随后重新绑定浮点数对象。

可以用一个简化的数值层次理解内置类型:

bool
  └── int
        └── Real
              └── Complex

这里的 RealComplexnumbers 模块中的抽象基类概念,不表示对象会被自动存储成某种统一格式。它们主要用于描述一个对象支持哪些数值行为。

from numbers import Integral, Real, Complex

print(isinstance(True, Integral))   # True
print(isinstance(10, Integral))     # True
print(isinstance(10.0, Real))       # True
print(isinstance(1 + 2j, Complex))  # True

boolint 的子类,因此:

print(isinstance(True, int))  # True
print(True == 1)              # True
print(False == 0)             # True
print(type(True) is int)      # False
print(type(True) is bool)     # True

这里必须区分两件事:

  • isinstance(True, int) 询问的是继承关系;
  • type(True) is int 询问的是精确类型。

bool 只有 TrueFalse 两个实例值,且在需要整数的上下文中分别表现为 10。这解释了为什么 True + 2 的结果是 3,但不能因此把所有布尔值都当成普通整数使用。(docs.python.org)


2. 字面量如何决定数值类型

2.1 整数字面量:十进制、二进制、八进制与十六进制

不含小数点和指数的数字字面量会创建 int

a = 42
b = 0b101010
c = 0o52
d = 0x2A

print(a, b, c, d)
print({type(a), type(b), type(c), type(d)})

输出:

42 42 42 42
{<class 'int'>}

不同进制只是源码表示不同,运行时都得到整数值。下划线可以提高长数字的可读性:

budget = 1_000_000
mask = 0b1111_0000

下划线不会改变数值:

print(1_000_000 == 1000000)  # True

整数字面量本身没有固定的 32 位或 64 位上限。Python 的 int 具有任意精度,算术运算会在需要时使用更多空间,而不是像固定宽度整数那样自然回绕。(docs.python.org)

n = 10 ** 100
print(len(str(n)))  # 101

这段代码中的 n 是精确整数,不是近似值。


2.2 浮点数字面量:小数点或指数会产生 float

含有小数点或指数标记的数字字面量会创建 float

x = 3.14
y = 314e-2
z = 1.

print(type(x))  # <class 'float'>
print(x == y)   # True
print(type(z))  # <class 'float'>

科学计数法中的 eE 表示十进制指数:

314e2=314×102=3.14314e^{-2} = 314 \times 10^{-2} = 3.14

float 的内部表示通常是二进制浮点数。十进制小数是否能被二进制有限位精确表示,取决于其分母约分后是否只含因子 2。

例如:

0.5=120.5 = \frac{1}{2}

可以精确表示;而:

0.1=110=12×50.1 = \frac{1}{10} = \frac{1}{2 \times 5}

包含因子 5,通常不能用有限长度的二进制小数精确表示。

print(0.1 + 0.2)          # 0.30000000000000004
print((0.1 + 0.2) == 0.3) # False

这不是 Python 独有的语法错误,而是有限精度二进制浮点表示的结果。可以通过 as_integer_ratio() 查看某个 float 实际代表的精确有理数:

print((0.1).as_integer_ratio())

典型输出:

(3602879701896397, 36028797018963968)

也就是说,0.1 这个 float 实际保存的是:

360287970189639736028797018963968\frac{3602879701896397}{36028797018963968}

而不是数学上的精确 1/101/10float.as_integer_ratio() 返回的是与当前浮点值完全相等的最简整数比;对无穷大和 NaN 使用该方法会分别产生 OverflowErrorValueError。(docs.python.org)


2.3 复数字面量:j 表示虚部

Python 使用 jJ 表示虚数单位:

z = 3 + 4j

print(type(z))  # <class 'complex'>
print(z.real)   # 3.0
print(z.imag)   # 4.0

这里的数学含义是:

z=3+4iz = 3 + 4i

Python 代码中写作 j,但其数学意义仍然是虚数单位 ii

3j 是实部为零的复数:

print(3j)        # 3j
print((3j).real) # 0.0
print((3j).imag) # 3.0

整数或浮点数与复数相加时,会被纳入复数运算:

print(2 + 3j)    # (2+3j)
print(2.5 + 3j)  # (2.5+3j)

复数的实部和虚部都是浮点数,因此即使输入实部使用整数,读取 .real.imag 时也会得到 float。(docs.python.org)


2.4 bool 不是由 01 字面量创建的

a = 0
b = False

print(type(a))  # <class 'int'>
print(type(b))  # <class 'bool'>

False0 比较相等,是因为布尔类型继承自整数类型并遵循相应的数值关系;但二者仍然是不同类型的对象。


3. int:任意精度不等于无成本

3.1 任意精度整数如何表现

固定宽度整数常见这样的行为:

最大值 + 1 -> 最小值

Python int 不会因为普通算术自然溢出:

n = 2 ** 1000
print(n > 0)  # True

但是“任意精度”只表示数值范围不受固定机器字长直接限制,并不表示操作没有资源边界。整数越大,所需内存越多,加法、乘法、转换为字符串等操作也可能变慢。

n = 10 ** 1_000_000

这类代码可能消耗大量内存和 CPU。生产系统仍然需要根据输入大小设置资源限制。


3.2 除法:/ 总是产生浮点结果

对整数使用 /,结果是 float

print(5 / 2)       # 2.5
print(type(5 / 2)) # <class 'float'>

这意味着整数除法可能在转换到浮点时遇到边界:

n = 10 ** 400

try:
    print(n / 1)
except OverflowError as exc:
    print(type(exc).__name__, exc)

在常见 Python 实现中,整数与浮点数进行混合算术时,需要把整数转换到浮点数表示;如果整数超出浮点数可表示范围,就可能产生 OverflowError。整数本身仍然可以保存该值,失败发生在“转换为浮点参与运算”这一步。

如果业务要求结果仍是精确整数,应使用 //divmod() 或分数模型,而不是先使用 / 再转换。


3.3 // 是向负无穷取整,不是向零截断

Python 的整除定义为:

x=(x//y)×y+(x%y)x = (x // y) \times y + (x \% y)

并且要求余数与除数同号或为零。对于正数,结果和“截去小数部分”相同:

print(5 // 2)  # 2
print(5 % 2)   # 1

负数时差异变得重要:

print(-5 // 2) # -3
print(-5 % 2)  # 1

推导过程:

52=2.5-\frac{5}{2} = -2.5

向负无穷取整得到:

5//2=3-5 // 2 = -3

为了满足恒等式:

5=(3)×2+1-5 = (-3) \times 2 + 1

所以余数是 1

如果想要向零截断,应使用 int()math.trunc()

import math

print(int(-2.5))       # -2
print(math.trunc(-2.5)) # -2
print(math.floor(-2.5)) # -3
print(math.ceil(-2.5))  # -2

四者的语义不同:

操作 -2.5 的结果
int(-2.5) -2
math.trunc(-2.5) -2
math.floor(-2.5) -3
math.ceil(-2.5) -2

Python 对 // 的定义是向负无穷方向取整;从 float 转换到 int 则是丢弃小数部分,也就是向零截断。(docs.python.org)


3.4 %divmod() 应成对理解

q, r = divmod(-5, 2)

print(q)           # -3
print(r)           # 1
print(q * 2 + r)   # -5

divmod(x, y) 等价于:

(x // y, x % y)

对于分页、分桶、周期索引等算法,负数输入是否允许必须明确。直接把 % 当作“数学余数”往往会掩盖符号约定;在 Python 中,-5 % 21 是语言定义的结果,不是实现偶然行为。(docs.python.org)


3.5 幂运算与负指数

整数的非负整数次幂仍然是整数:

print(2 ** 10)  # 1024
print(type(2 ** 10))  # <class 'int'>

但负指数会产生浮点数:

print(2 ** -1)       # 0.5
print(type(2 ** -1)) # <class 'float'>

原因是:

21=122^{-1} = \frac{1}{2}

而普通 / 运算的结果类型是 float

Python 定义 0 ** 01

print(0 ** 0)  # 1

这遵循编程语言中常用的幂运算约定,并不意味着所有数学语境都把 000^0 当作无争议的数学值。(docs.python.org)


4. 位运算:Python int 的“无限符号位”模型

位运算只适用于整数:

a = 0b1100
b = 0b1010

print(bin(a & b))  # 0b1000
print(bin(a | b))  # 0b1110
print(bin(a ^ b))  # 0b0110
print(bin(~a))     # -0b1101

Python 对整数位运算的定义,可以理解为使用“带无限符号扩展位的二进制补码”计算。对于非负数,这与直觉中的二进制位操作一致;对负数,不能简单套用固定 8 位或 32 位的位图。

一元按位取反满足:

x=x1\sim x = -x - 1

因此:

print(~5)   # -6
print(~-6)  # 5

移位也有明确的数学含义:

print(5 << 3)   # 40
print(40 >> 3)  # 5
print(-5 >> 1)  # -3

左移:

x<<n=x×2nx << n = x \times 2^n

右移:

x>>n=x2nx >> n = \left\lfloor \frac{x}{2^n} \right\rfloor

所以负数右移仍然是向负无穷取整:

5>>1=2.5=3-5 >> 1 = \left\lfloor -2.5 \right\rfloor = -3

负移位数量非法:

try:
    1 << -1
except ValueError as exc:
    print(type(exc).__name__)  # ValueError

Python 不会把整数位宽自动限制为 8 位、32 位或 64 位;如果协议、文件格式或硬件寄存器要求固定宽度,必须显式使用掩码或字节转换。(docs.python.org)


5. float:有限精度、舍入、无穷大与 NaN

5.1 机器浮点数的基本边界

Python 文档说明,float 通常使用 C 语言中的双精度浮点格式;当前解释器的有效精度、最大值、最小正规格等信息可通过 sys.float_info 查询。(docs.python.org)

import sys

print(sys.float_info.max)
print(sys.float_info.epsilon)
print(sys.float_info.dig)

这些字段描述的是当前运行环境中的浮点能力,而不是 Python 语言对所有平台强制规定的相同硬件格式。因此,需要依赖浮点边界时,应在目标环境检查,而不要硬编码假设。


5.2 为什么 0.1 + 0.2 不等于 0.3

浮点数运算通常可以抽象为:

fl(xy)\operatorname{fl}(x \circ y)

其中:

  • xxyy 是浮点近似值;
  • \circ 是加法、乘法等运算;
  • fl\operatorname{fl} 表示将真实结果舍入到可表示的浮点数。

因此,下面的过程不是:

0.1+0.2=0.30.1 + 0.2 = 0.3

而更接近:

fl(fl(0.1)+fl(0.2))\operatorname{fl}(\operatorname{fl}(0.1) + \operatorname{fl}(0.2))

如果程序需要判断“足够接近”,应根据误差模型使用 math.isclose(),而不是盲目使用固定小数位比较:

import math

result = 0.1 + 0.2
print(math.isclose(result, 0.3))  # True

但“接近”不是“相等”。例如金额、计量结果或协议字段的精度要求必须由业务规则决定,不能由 math.isclose() 的默认容差替代。


5.3 round() 不是传统的“逢五进一”

Python 的 round() 对半数采用“舍入到偶数”:

print(round(2.5))  # 2
print(round(3.5))  # 4
print(round(1.25, 1))  # 1.2
print(round(1.35, 1))  # 1.4

2.5

  • 相邻整数是 23
  • 2 是偶数;
  • 所以结果为 2

3.5

  • 相邻整数是 34
  • 4 是偶数;
  • 所以结果为 4

但是 round(1.35, 1) 的行为还受到 1.35 本身并非通常意义上的精确十进制值这一事实影响。因此不能把 round(x, 2) 理解为“先精确得到十进制金额,再按两位舍入”。

round() 在不提供 ndigits 时返回整数;提供 ndigits 时通常返回与输入数值类型相同或相容的结果。对于要求明确十进制舍入模式的场景,应使用 decimal.Decimal 配合上下文和舍入模式,而不是依赖二进制 float。(docs.python.org)


5.4 int(float) 是截断,不是四舍五入

print(int(3.9))   # 3
print(int(-3.9))  # -3

int() 丢弃小数部分,方向是向零:

print(int(3.9) == 3)
print(int(-3.9) == -3)

如果需求是:

  • 向下取整:math.floor()
  • 向上取整:math.ceil()
  • 向零截断:int()math.trunc()
  • 按指定舍入规则:round()Decimal

这些操作不能互相替代。


5.5 infnan 不是普通数字

Python 可以构造无穷大和 NaN:

import math

positive_inf = float("inf")
negative_inf = float("-inf")
not_a_number = float("nan")

print(math.isinf(positive_inf))  # True
print(math.isnan(not_a_number))  # True

NaN 的特殊之处在于,它不等于包括自身在内的任何值:

nan = float("nan")

print(nan == nan)  # False
print(nan != nan)  # True

因此,不能这样判断 NaN:

if value == float("nan"):
    ...

应使用:

if math.isnan(value):
    ...

无穷大也可能传播:

print(float("inf") + 100)  # inf
print(float("inf") - float("inf"))  # nan

把浮点数转换为整数时,这些特殊值会触发异常:

for value in [float("inf"), float("-inf"), float("nan")]:
    try:
        print(int(value))
    except (OverflowError, ValueError) as exc:
        print(type(exc).__name__)

输出通常为:

OverflowError
OverflowError
ValueError

因此,外部输入进入整数边界、数组下标、分页参数或数据库字段前,应明确处理 NaN 和无穷大,而不能只检查 isinstance(value, float)


5.6 用十六进制形式检查浮点值

由于十进制字符串与二进制浮点之间通常需要舍入,调试浮点边界时可以使用十六进制形式:

x = 0.1
text = x.hex()

print(text)
print(float.fromhex(text) == x)  # True

float.hex() 可以给出当前有限浮点值的精确十六进制表示,float.fromhex() 可以恢复同一个浮点值。这种形式适合定位“两个值打印相同但底层表示不同”或跨语言传递浮点值的情况。(docs.python.org)


6. complex:代数运算成立,大小排序不成立

6.1 复数的四则运算

设:

z1=a+bi,z2=c+diz_1 = a + bi,\qquad z_2 = c + di

则:

z1+z2=(a+c)+(b+d)iz_1 + z_2 = (a+c) + (b+d)i

z1z2=(acbd)+(ad+bc)iz_1 z_2 = (ac-bd) + (ad+bc)i

Python 直接实现这些规则:

z1 = 2 + 3j
z2 = 4 - 1j

print(z1 + z2)  # (6+2j)
print(z1 * z2)  # (11+10j)

乘法推导如下:

(2+3i)(4i)=82i+12i3i2(2+3i)(4-i) = 8 - 2i + 12i - 3i^2

因为 i2=1i^2=-1,所以:

=8+10i+3=11+10i= 8 + 10i + 3 = 11 + 10i

Python 将复数的实部和虚部存储为浮点数,因此过大的实部或虚部仍然可能受到浮点范围和精度影响。(docs.python.org)


6.2 复数没有自然的 <>

复数支持相等性比较:

print((1 + 2j) == (1 + 2j))  # True
print((1 + 2j) == (1 + 3j))  # False

但不支持通常意义上的大小排序:

try:
    print(1 + 2j < 2 + 3j)
except TypeError as exc:
    print(type(exc).__name__)  # TypeError

原因不是 Python “缺少一个实现”,而是复数在数学上没有与实数相同的、同时保持通常加法和乘法性质的全序关系。若需要按照复数大小排序,必须先定义业务含义,例如按模长:

a+bi=a2+b2|a+bi| = \sqrt{a^2+b^2}

values = [3 + 4j, 1 + 1j, 5 + 0j]

ordered = sorted(values, key=abs)
print(ordered)

这里排序依据是模长,而不是复数本身的大小。


6.3 复数不支持 //%divmod()

try:
    print((3 + 4j) // 2)
except TypeError as exc:
    print(type(exc).__name__)  # TypeError

整除和余数依赖“商向下取整”的顺序概念,而复数没有这种自然顺序。若要计算复数的距离、幅度或阈值,应使用 abs(z);若要进行复数函数运算,可使用 cmath。内置数值操作中,//%divmod() 不适用于复数。(docs.python.org)


6.4 共轭、模长与实部虚部

复数 z=a+biz=a+bi 的共轭为:

z=abi\overline{z}=a-bi

Python:

z = 3 + 4j

print(z.real)        # 3.0
print(z.imag)        # 4.0
print(z.conjugate()) # (3-4j)
print(abs(z))        # 5.0

模长满足:

z=a2+b2|z| = \sqrt{a^2+b^2}

因此:

3+4i=32+42=5|3+4i|=\sqrt{3^2+4^2}=5

比较复数时,如果业务含义是“距离原点的远近”,应比较 abs(z);如果含义是“实部优先、虚部次之”,则应显式写出元组键:

ordered = sorted(values, key=lambda z: (z.real, z.imag))

不要依赖隐含的复数排序,因为该操作本身会失败。


7. 混合数值运算:窄类型向宽类型提升

Python 对内置数值类型支持混合算术。可以把类型提升概括为:

int + int       -> int
int + float     -> float
int + complex   -> complex
float + complex -> complex

示例:

print(type(1 + 2))       # int
print(type(1 + 2.0))     # float
print(type(1 + 2j))      # complex
print(type(1.0 + 2j))    # complex

规则可以表述为:

  1. 两个操作数都是 complex:不再转换;
  2. 任一操作数是 complex:另一个转换到复数运算;
  3. 任一操作数是 float:另一个转换到浮点运算;
  4. 否则两个操作数都是整数。

文档使用“更窄类型向更宽类型提升”描述这种行为。(docs.python.org)

需要特别注意:bool 由于是 int 子类,会参与整数级别的运算:

print(True + 2)       # 3
print(False * 100)    # 0
print(1.5 + True)     # 2.5
print((True + 2j))    # (1+2j)

bool 不会形成新的“布尔算术类型”。运算结果一般是 intfloatcomplex

result = True + True

print(result)        # 2
print(type(result))   # <class 'int'>

8. 类型转换:语法相似,语义不同

8.1 int() 的输入边界

print(int("42"))      # 42
print(int("101", 2))  # 5
print(int(3.9))       # 3
print(int(True))      # 1

字符串转换可以指定进制:

print(int("ff", 16))  # 255
print(int("0b101", 2)) # 5

但浮点转整数是截断,不是舍入。非法字符串会产生 ValueError

try:
    int("3.14")
except ValueError as exc:
    print(type(exc).__name__)  # ValueError

如果输入可能来自用户或网络,不应直接假设 int() 一定成功。


8.2 float() 的输入边界

print(float("3.14"))  # 3.14
print(float("inf"))   # inf
print(float("nan"))   # nan
print(float(10))      # 10.0

但大整数转浮点可能超出浮点范围:

n = 10 ** 400

try:
    print(float(n))
except OverflowError as exc:
    print(type(exc).__name__)

float() 的成功只说明输入可以映射到一个浮点对象,不说明转换保持了原整数的全部精度。

Python 3.14 新增了 float.from_number(x)。它面向数值对象,优先调用 x.__float__(),若没有则回退到 x.__index__();整数或浮点数转换时仍然受到 Python 浮点精度与范围限制。(docs.python.org)

print(float.from_number(10))  # 10.0

这是 Python 3.14 的版本敏感能力;编写兼容旧版本的库时,不能无条件假设该类方法存在。


8.3 complex() 的两种主要形式

可以传入实部和虚部:

print(complex(3, 4))  # (3+4j)

也可以解析复数字符串:

print(complex("3+4j"))  # (3+4j)

需要注意,字符串形式必须符合复数字面量的语法;外部输入不符合格式时会失败。

Python 3.14 还新增了 complex.from_number(x)。它会优先使用 x.__complex__(),再依次回退到 __float__()__index__()。(docs.python.org)


9. 比较、相等与哈希:数值相等不等于类型相同

9.1 数值类型之间可以比较相等性

print(1 == 1.0)       # True
print(1 == True)      # True
print(0 == False)     # True
print(1 + 0j == 1)    # True

Python 对不同数值类型的比较,按照它们代表的数值进行比较,而不是要求类型完全一致。(docs.python.org)

但是,==is 完全不同:

x = 1000
y = 1000

print(x == y)  # True:值相等
print(x is y)  # 不应依赖:是否为同一个对象

数值判断通常使用 ==;对象身份判断才使用 is。不要用对象身份判断两个数字是否相等。


9.2 相等的数值必须具有相同哈希值

Python 要求:

x==yhash(x)==hash(y)x == y \Rightarrow hash(x) == hash(y)

因此:

print(hash(1) == hash(1.0))  # True
print(hash(True) == hash(1)) # True

这会影响字典键和集合:

data = {
    1: "integer",
    True: "boolean",
    1.0: "float",
}

print(data)
print(len(data))

典型结果是字典只有一个键,因为 1True1.0 既相等又具有相同哈希值,后写入的值会覆盖先写入的值。

因此,如果系统必须区分“整数 1”“布尔 True”和“浮点 1.0”,不能只把它们直接作为字典键;应显式加入类型信息:

def typed_key(value):
    return type(value), value

data = {
    typed_key(1): "integer",
    typed_key(True): "boolean",
    typed_key(1.0): "float",
}

print(data)

复数的哈希则由实部和虚部的哈希组合得到。数值哈希规则是跨 intfloatFractionDecimal 等数值类型协调设计的。(docs.python.org)


10. 布尔运算与数值真值测试

任何对象都可以进行真值测试。对于数值类型,零值为假,非零值为真:

for value in [0, 0.0, 0j, 1, -1, 0.1]:
    print(value, bool(value))

输出:

0 False
0.0 False
0j False
1 True
-1 True
0.1 True

特别要注意:

print(bool(float("nan")))  # True
print(bool(float("inf")))  # True

NaN 虽然不是一个正常的数值,但它不是零,因此真值测试结果为 True。如果业务需要判断“是否存在有效测量值”,应单独检查 math.isnan()math.isfinite()

andor 返回的是操作数本身,不一定返回布尔值:

print(0 or 5)       # 5
print(3 and 5)      # 5
print(0 and 5)      # 0

规则是:

  • x or y:如果 x 为真,返回 x;否则返回 y
  • x and y:如果 x 为假,返回 x;否则返回 y

它们还具有短路求值特性:不需要计算的右操作数不会执行。(docs.python.org)

这段代码中的 or 不是“把两个值转换后做逻辑或”:

timeout = configured_timeout or 30

如果 configured_timeout0,它会被当作假值并替换为 30。如果 0 是合法配置,这段代码就改变了业务含义,应改为显式判断 None

timeout = 30 if configured_timeout is None else configured_timeout

11. 运算优先级:先确认语法树,再解释结果

数值边界有时不是类型问题,而是表达式分组问题。

print(-2 ** 2)    # -4
print((-2) ** 2)  # 4

Python 将第一句理解为:

-(2 ** 2)

而不是:

(-2) ** 2

幂运算还具有右结合性:

print(2 ** 3 ** 2)    # 512
print(2 ** (3 ** 2))  # 512

它不是:

(2 ** 3) ** 2  # 64

当表达式同时包含负号、幂、位运算和比较时,应使用括号表达意图:

result = (-base) ** exponent
mask = (value & MASK) == expected

括号不仅提高可读性,也避免把运算优先级误认为数学记号中的默认分组。


12. 整数、浮点数和复数的边界对照

类型 主要表示 精度特征 支持排序 典型失败边界
int 整数 任意精度 支持 内存、时间、转浮点溢出
float 二进制浮点数 有限精度 支持,但 NaN 会破坏直觉 舍入误差、infnan
complex 实部与虚部 两个浮点分量 不支持 <> 无序比较、浮点分量范围
bool True / False 语义上是逻辑值,类型上是 int 子类 支持整数关系 隐式参与算术、字典键碰撞

这个表不能替代具体语义。例如,int 的“任意精度”并不意味着可以安全地转换为 floatfloat 的“支持排序”也不意味着含 NaN 的数据可以直接获得全序;bool 的“是整数子类”也不意味着应该用 True 代替所有数值 1


13. 真实失败路径与诊断方法

13.1 需要精确整数,却意外进入浮点

n = 10 ** 400

try:
    result = n / 1
except OverflowError as exc:
    print("浮点转换失败:", type(exc).__name__)

诊断思路:

  1. 确认原始值是否仍为 int
  2. 检查表达式中是否出现 /、浮点字面量或 float()
  3. 如果目标是精确商,改用 Fractiondivmod() 或整数缩放;
  4. 如果目标确实是浮点结果,先定义超范围时的业务处理。

13.2 浮点比较失败

actual = 0.1 + 0.2
expected = 0.3

print(actual == expected)  # False

诊断代码:

import math

print(actual.hex())
print(expected.hex())
print(actual - expected)
print(math.isclose(actual, expected))

如果结果用于控制流程,应明确:

  • 是要求数学上严格相等;
  • 还是允许绝对误差;
  • 还是允许相对误差;
  • 是否必须排除 NaN 和无穷大。

math.isclose() 适合近似比较,但不能自动解决单位错误、累计误差过大或错误的舍入时机。


13.3 复数排序失败

values = [1 + 2j, 2 + 1j]

try:
    print(sorted(values))
except TypeError as exc:
    print(type(exc).__name__)  # TypeError

修复方式取决于领域定义:

print(sorted(values, key=abs))
print(sorted(values, key=lambda z: (z.real, z.imag)))

第一种按模长排序,第二种按实部和虚部的字典序排序。二者都不是“复数自然排序”,而是程序员明确选择的投影或排序规则。


13.4 NaN 进入统计或过滤流程

values = [1.0, 2.0, float("nan"), 4.0]

print(max(values))  # 结果不应被当作可靠统计结论

含 NaN 的比较会产生非直觉结果,统计函数是否传播 NaN 也取决于具体 API。进入计算前应先清洗:

import math

valid_values = [
    value for value in values
    if math.isfinite(value)
]

print(valid_values)  # [1.0, 2.0, 4.0]

如果无穷大在业务中是合法哨兵值,则不能简单使用 isfinite() 丢弃它,而应分别定义 NaN、正无穷和负无穷的业务语义。


14. 何时不应继续使用 float

float 适合:

  • 科学计算中的近似值;
  • 图形、信号和测量数据;
  • 允许误差的比例、坐标和物理量;
  • 对性能和硬件浮点运算友好的场景。

但以下场景通常需要其他数值模型:

14.1 货币和十进制定点金额

from decimal import Decimal

price = Decimal("0.10")
quantity = Decimal("3")

print(price * quantity)  # 0.30

关键在于从字符串构造 Decimal,而不是先构造 float

print(Decimal("0.1"))
print(Decimal(0.1))

后者会把已经存在的二进制浮点近似值带入 DecimalDecimal 的精度、舍入模式和上下文适合需要十进制规则的场景,但它并不是本文四种内置类型之一,且运算混用时需要显式处理类型边界。

14.2 需要精确有理数

from fractions import Fraction

value = Fraction(1, 10) + Fraction(2, 10)

print(value)        # 3/10
print(float(value)) # 0.3

Fraction 可以保持有理数的精确分子和分母;转换为 float 的瞬间,仍会回到浮点精度边界。

这也是数值模型选择的核心:先决定“值的数学含义”,再决定存储类型,而不是看到输入有小数就自动使用 float


15. 与随机数和密码学边界的连接

数值类型还会影响随机数 API 的使用方式。

普通伪随机数常用于模拟、抽样和测试:

import random

random.seed(1234)
print(random.randrange(10))

这里的整数参数表示范围边界,随机状态由伪随机数生成器维护;固定种子通常用于复现实验,不应被误认为安全密钥来源。

需要生成密码、令牌或安全随机数时,应使用 secrets

import secrets

token = secrets.token_urlsafe(32)
print(token)

密码学安全随机数的核心不是“返回一个很大的 int”,而是随机源和生成器的安全性质。random 产生的结果即使输出类型是 int,也不能因此获得密码学安全性。

随机抽样中的边界也必须区分:

import random

print(random.randrange(5))  # 可能是 0、1、2、3、4

右端点 5 不包含在结果中;如果把它误当作闭区间,就会出现越界或分布错误。这个边界属于 API 语义,但最终仍然通过 int 的范围和比较规则表现出来。


16. 一组可执行的自检代码

下面的代码集中验证本文最容易混淆的边界:

import math
from decimal import Decimal
from fractions import Fraction

# 类型
print(type(1).__name__)       # int
print(type(1.0).__name__)     # float
print(type(1 + 2j).__name__)  # complex
print(type(True).__name__)    # bool

# bool 是 int 的子类
print(isinstance(True, int))  # True
print(True + 2)               # 3

# 混合运算
print(type(1 + 1.0).__name__)     # float
print(type(1 + 1j).__name__)      # complex

# 整除与余数
print(-5 // 2, -5 % 2)        # -3 1
print(divmod(-5, 2))          # (-3, 1)

# 浮点误差
print(0.1 + 0.2 == 0.3)       # False
print(math.isclose(0.1 + 0.2, 0.3))  # True

# 舍入
print(round(2.5))             # 2
print(round(3.5))             # 4

# 复数
z = 3 + 4j
print(z.real, z.imag)         # 3.0 4.0
print(abs(z))                 # 5.0

# NaN
nan = float("nan")
print(math.isnan(nan))        # True
print(nan == nan)             # False

# Decimal 与 Fraction
print(Decimal("0.1") + Decimal("0.2"))  # 0.3
print(Fraction(1, 10) + Fraction(2, 10)) # 3/10

运行这些示例时,重点不是记住每一行输出,而是观察每个结果背后的转换路径:

  • bool 参与整数运算;
  • / 把整数计算带入浮点模型;
  • // 依赖向负无穷取整;
  • float 可能不精确表示十进制小数;
  • complex 支持代数运算但不支持自然排序;
  • DecimalFraction 通过不同方式解决特定精度问题。

Python 数值类型的真正边界,不在于类型名称本身,而在于“数学值如何被表示、转换和比较”。int 保证整数运算的任意精度,却不能保证转换到 float 后仍然精确;float 提供高效的近似实数,却可能产生舍入误差、NaN 和无穷大;complex 扩展了代数运算,却放弃了实数意义下的大小排序;bool 只有逻辑语义,却由于继承关系能够进入整数计算。

当表达式跨越这些边界时,应先写出需要保持的性质:精确整数、近似实数、十进制金额、有理数、复数模长、逻辑真假,还是固定宽度字节表示。只有先确定性质,再选择类型和运算符,代码的结果才不会被 Python 的隐式转换和数学直觉之间的差异所左右。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。