Skip to content
This page has been auto-translated and may contain errors.View in English

数字与算术

数字几乎出现在你写的每一个程序中。购物车计算总价。游戏更新分数。脚本统计某件事发生了多少次。Python 提供了像纸上数学一样工作的算术运算符,还有几个从一开始就值得了解的运算符。

Python 的算术运算符涵盖了标准的运算符集合,还包括整数除法、取模和幂运算。一些行为与其他语言不同,并且在实际中很重要:/ 总是返回浮点数,整除向负无穷取整,而取模遵循真正的模数语义。

Python 的数字塔:int(任意精度)、float(IEEE 754 binary64)、complex(此处不涉及)。算术运算符遵循数学定义而非 C 约定:// 是整除(向负无穷取整),% 带有除数的符号,两者结合对所有整数输入满足恒等式 a == (a // b) * b + (a % b)

运算符

数学中的四个运算符(+-*/)的工作方式完全符合你的预期。Python 增加了三个你会经常使用的运算符:整数除法、取余和幂运算。

标准的四个运算符按预期工作,但有一条值得注意的规则:/ 总是返回浮点数,即使结果是整数。三个额外的运算符让你能够无需额外工作就能表达更多。

全部七个运算符都映射到 dunder 方法:+ 对应 __add__// 对应 __floordiv__% 对应 __mod__** 对应 __pow__,等等。混合的 int/float 运算会扩展为 float。无论操作数类型如何,/ 总是返回 float

python
price    = 12.99
quantity = 3

print(price * quantity)   # 38.97
print(price + 2)          # 14.99
print(price - 1.00)       # 11.99
运算符名称示例结果
+加法5 + 38
-减法5 - 32
*乘法5 * 315
/除法5 / 31.6666...
//整数除法5 // 31
%取余5 % 32
**幂运算5 ** 3125

除法:///

/ 总是给你精确的十进制结果,即使答案是整数。// 只给你整数部分,舍去小数点后的所有内容。它不是四舍五入,而是直接截断:

无论输入是否为整数,/ 总是返回 float// 返回结果向下取整:小于或等于真实结果的最大整数。对于正数,这与截断相同。对于负数则不同:

/ 是真除法:总是返回 float// 是整除:它对真实商应用 math.floor(),总是向负无穷方向取整而非向零取整。这与 C 和 Java 不同,后者中整数除法是截断的。数学上的好处是:Python 的 //% 对所有整数输入(包括负数)都满足恒等式 a == (a // b) * b + (a % b)。C 的截断除法对负数破坏了这个恒等式。

python
10 / 2     # 5.0   (总是 float,即使能整除)
10 / 3     # 3.3333333333333335

10 // 3    # 3
7  // 2    # 3
-7 // 2    # -4    (向负无穷取整,而非向零取整)

-7 // 2 的结果让人惊讶。你大多数情况下会对正数使用 //,这时不会出现这种情况。把它记在脑海里,当负数出现时备用。

Python 把这称为整除(floor division),因为它应用了数学上的向下取整函数。其他语言则是向零截断,对负数会给出不同的结果。// 这个名字是个提示:先除,再取整。

// 实现的是 floor(a / b),而非截断。恒等式 a == (a // b) * b + (a % b) 对 Python 中所有整数输入都成立。在 C 和 Java 中,/ 向零截断,这个恒等式对负数失败,并且 % 作为余数运算符(取被除数的符号)而非真正的模数(取除数的符号)。

取余运算符 %

% 给你整数除法之后剩下的数。如果 10 // 33(因为 3 能在 10 中放下三次),那么 10 % 3 就是 1(因为 3 × 3 = 9,而 10 - 9 = 1)。最常见的用途是检查一个数是奇数还是偶数。

% 是取模运算符。检查奇偶是显而易见的用法,但它推广到任何循环或环绕问题:将计数器保持在一个范围内、把项目分配到组中、重复一个序列。模式始终是 value % limit,返回介于 0limit - 1 之间的值。

Python 的 % 是真正的模数:结果总是带有除数的符号。这与 C 和 Java 不同,那里的 % 是余数运算符,取被除数的符号。在 Python 中,-7 % 32,而不是 -1,因为模数定义为 a - (a // b) * b,而 // 向负无穷取整。这种一致的符号行为使得 % 在处理负数输入的循环和环绕时是可靠的。

python
10 % 3    # 1
10 % 2    # 0  (能整除)
10 % 7    # 3

6 % 2     # 0  (偶数)
7 % 2     # 1  (奇数)

幂运算 **

** 将一个数提升到某次幂。使用两个星号,而不是 ^ 符号(在 Python 中表示其他意思):

** 是幂运算。它也适用于浮点数,这样你可以用分数次幂来表达根,而不需要单独调用函数:

** 调用 __pow__。两个 int 操作数时返回 int;任一 float 操作数时返回 float。一个优先级陷阱:-2 ** 2 解析为 -(2 ** 2),因为 ** 比一元负号绑定得更紧,结果是 -4 而不是 4。使用括号:(-2) ** 2

python
2 ** 10    # 1024
3 ** 3     # 27
9 ** 0.5   # 3.0  (平方根:提升到 0.5 次幂)

运算符优先级

Python 遵循标准的数学顺序:先幂运算,然后乘除,最后加减。当你不确定时,使用括号。它们让意图清晰且没有代价:

Python 遵循标准的 PEMDAS/BODMAS 顺序。让人困惑的部分:///% 都共享同样的优先级,混用时从左到右求值。括号是免费的;当顺序一眼看不明显时就使用它们:

算术运算符的优先级从高到低:**,然后一元 -,然后 * / // %(同优先级从左到右),然后 + -。一元负号与 ** 的交互是一个微妙的陷阱:-2 ** 2-(2 ** 2) = -4,因为一元负号比 ** 绑定得更松。在负号与幂运算结合时务必加括号。

python
2 + 3 * 4      # 14,而不是 20
2 ** 3 + 1     # 9,而不是 512
10 - 4 / 2     # 8.0,而不是 3.0

(2 + 3) * 4    # 20
10 / (2 + 3)   # 2.0

intfloat 的相互作用

Python 有一个一致的规则:/ 总是返回小数(即使 4 / 2 也给出 2.0),任何混合整数和小数的运算都会得到小数。当你需要整数时,使用 // 或用 int() 进行转换。

类型规则是可预测的:/ 总是返回 float。两个整数的 //% 返回 int。任何混合 intfloat 的运算返回 float。这意味着 4 / 22.0,而不是 2,当你需要整数时(例如用作索引)这点很重要。

类型强制遵循固定的层次结构:在混合运算中 int 扩展为 float/ 映射到 __truediv__,总是返回 float// 映射到 __floordiv__:两个 int 操作数时返回 int;任一 float 操作数时返回 float。这些规则是一致且可预测的;唯一令人惊讶的是即使是 4 / 2/ 也从不返回 int

python
4 / 2      # 2.0   (总是 float)
4 // 2     # 2     (int)
4 + 2      # 6     (int)
4 + 2.0    # 6.0   (float)
4 * 0.5    # 2.0   (float)

浮点精度

有一个几乎每个人在某个时刻都会遇到的陷阱:

python
0.1 + 0.2   # 0.30000000000000004

这个微小的误差不是 Python 的 bug。计算机用二进制存储小数,而像 0.1 这样的某些值无法精确表示。这类似于 1/3 无法用十进制精确表示。对大多数日常计算来说这并不重要。对于显示金额,round():.2f 格式说明符可以让输出保持整洁。

Python 的浮点数是 IEEE 754 binary64:64 位,大约有 15-16 个有效十进制数字的精度。不精确的现象出现是因为某些分数无法在二进制中精确表示。0.1 + 0.2 产生 0.30000000000000004。只有在检查原始值时才会显示出偏差;用 :.2fround() 格式化时会在输出中隐藏。

对于分币累积的金融工作,Python 在标准库中提供 decimal.Decimal,具有精确的十进制算术。这将在 模块 章节中介绍。

float 是 IEEE 754 binary64:sign × mantissa × 2^exponent,53 位尾数,相对精度为 2^-52 ≈ 2.2e-16。任何分母含有 2 以外质因数的分数(如 1/10 = 1/(2×5))都是非终止的二进制分数,无法精确存储。误差很小,但在重复的算术中会累积。

对于精确的十进制算术,Python 的 decimal.Decimal 在内部使用任意精度的十进制。对于完全不四舍五入的精确有理数算术,fractions.Fraction 存储分子/分母对。两者都在标准库中,将在 模块 章节中介绍。

可读的数字字面量

Python 允许你在数字字面量中放下划线,让大数更易阅读。Python 会完全忽略它们;它们只是给你看的:

下划线可以放在数字字面量的任何位置,在解析时会被剥离,对值没有影响。对于常量中的千位分隔符以及二进制或十六进制字面量中的数字分组很有用:

数字字面量中的下划线是分词器特性:在词法分析期间剥离,对结果值零影响。在整数、浮点数和带基数的字面量中都有效(0xFF_FF0b1010_00011_234.567_890)。唯一的限制:不能出现在开头、结尾或紧邻小数点或指数标记。

python
population  = 8_100_000_000
distance_km = 384_400
pi_approx   = 3.141_592_653

有用的内置函数

abs()

abs() 返回绝对值:无论输入的符号如何,结果总是正的。当你关心一个数离零有多远,而不是它在哪个方向时使用它。

abs() 返回一个数的大小。适用于整数和浮点数。对距离计算、误差范围以及任何方向无关而只需要数值大小的情况都很有用。

abs() 调用操作数的 __abs__。对于 intfloat,返回相同的类型。对于 complex,它返回模(与原点的欧几里得距离)作为 float。对实数而言,返回类型与输入类型一致。

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5

round()

round() 默认四舍五入到最近的整数。传递第二个参数可保留特定的小数位数:

python
round(3.7)          # 4
round(3.2)          # 3
round(3.14159, 2)   # 3.14

值得了解的一件事:round(2.5) 给出 2,而不是 3。当值恰好处于两个选项中间时,Python 会舍入到最近的偶数。

round() 使用银行家舍入法:当值恰好处于中间时,它舍入到最近的偶数而不是总是向上舍入。这在统计工作中能最小化累积误差,但如果你期望 0.5 总是向上舍入,可能会让你感到意外:

python
round(2.5)   # 2  (舍入到最近偶数)
round(3.5)   # 4
round(4.5)   # 4  (而不是 5)
round(3.14159, 2)   # 3.14

round() 实现 IEEE 754 的舍入到最近偶数(银行家舍入法):平局时舍入到最近的偶数整数。这与 "舍入一半向上" 的约定不同。带有 ndigits 参数时,round() 会调用对象的 __round__;自定义类型可以覆盖舍入行为。注意:由于浮点数不精确,像 round(2.5) 这样的"平局"在二进制中可能并不恰好落在 0.5,从而给出看似不一致的结果。

python
round(2.5)   # 2
round(3.5)   # 4
round(4.5)   # 4

divmod()

divmod() 在单次调用中同时返回商和余数。它返回一对值,你可以一次性赋给两个名字:

divmod(a, b) 等价于 (a // b, a % b),但是一步完成。当你需要两个值时使用它:分页、时间转换或将项目分配到组中。

divmod() 调用左操作数的 __divmod__。它执行一次除法并同时返回向下取整的商和模运算余数,避免了分别调用 //% 的冗余计算。结果对所有整数输入按 Python 的整除语义满足 a == divmod(a, b)[0] * b + divmod(a, b)[1]

python
divmod(10, 3)   # (3, 1):商 3,余数 1
divmod(7, 2)    # (3, 1)
divmod(9, 3)    # (3, 0)

quotient, remainder = divmod(10, 3)
print(quotient)    # 3
print(remainder)   # 1

(3, 1) 是什么?

那是一个元组:一对一起返回的固定值。元组有它自己的章节。现在,把这两个值通过一次性赋值给两个名字来拆开,如上所示。

实践中

一个小费计算器:

python
bill     = 45.50
tip_rate = 0.18
tip      = round(bill * tip_rate, 2)
total    = round(bill + tip, 2)

print(f"Bill:  ${bill}")
print(f"Tip:   ${tip}")
print(f"Total: ${total}")

round() 让输出看起来像金额,而不是一长串小数。

为分页计算页数并将进度跟踪为百分比:

python
total_items    = 153
items_per_page = 10

full_pages, leftover = divmod(total_items, items_per_page)
total_pages = (total_items + items_per_page - 1) // items_per_page

print(f"Full pages: {full_pages}, leftover: {leftover}")
print(f"Total pages needed: {total_pages}")   # 16
python
total_files     = 847
processed_files = 312

percent = round(processed_files / total_files * 100, 1)
print(f"Progress: {processed_files}/{total_files} ({percent}%)")

向上取整公式 (n + d - 1) // d 是一个不转换为浮点数就能向上取整的标准整数技巧。

最小-最大归一化和百分比变化:这两个模式在数据工作中经常出现:

python
# 最小-最大归一化:将值缩放到 0.0 到 1.0 范围内
value   = 75
minimum = 0
maximum = 100

normalised = (value - minimum) / (maximum - minimum)
print(f"Normalised: {normalised:.2f}")   # 0.75

# 两次测量之间的百分比变化
before = 1_200
after  = 1_380

change = (after - before) / before * 100
print(f"Change: {change:.1f}%")          # 15.0%

两种模式都归结为一个比率:相对于参考范围或参考量的值。float 的精度对大多数分析工作来说已经足够;累积误差只在计算链涉及数十次操作或值差异多个数量级时才会变得重要。