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

数字与算术运算 ​

几乎你写的每一个程序都会用到数字。购物车要算总价,游戏要更新分数,脚本要统计某件事发生了多少次。Python 提供的算术运算符跟你在纸上做数学一样直观,另外还有几个值得一开始就了解的运算符。

Python 的算术运算符涵盖了常规的一套,再加上整除、取模和幂运算。有几个行为跟其他语言不太一样,而且在实践中很重要:/ 永远返回浮点数,向下整除会朝负无穷方向取整,取模遵循真正的数学模运算语义。

Python 的整数没有溢出问题(也就是在固定大小整数的语言里会遇到的回绕或报错):它们可以随着内存允许一直增长下去。浮点数是常见的双精度小数,大多数场景够用,但用来表示金额永远不够精确。算术运算符遵循的是数学定义,而不是许多语言照搬的 C 语言惯例:// 是向下取整除法(朝负无穷方向取整),% 的符号跟除数一致,而不是跟被除数一致。这两个设计都保证了无论输入是正数还是负数,结果的规律都一致,这正是循环和环绕计算所需要的行为。

各种运算符 ​

数学中的四个基本运算符(+、-、*、/)表现跟你预期的完全一样。Python 还多加了三个你会经常用到的:整除、取余和幂运算。

标准的四个运算符行为跟预期一致,只有一条值得注意的规则:/ 永远返回浮点数,即使结果是整数。另外三个运算符可以让你不费额外功夫就表达更多计算逻辑。

每个运算符背后都对应一个双下划线方法(一种以双下划线命名、由 Python 在幕后调用的方法):+ 对应 __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
Juno各种运算符+、-、*、/ 的表现就跟纸上算数一样。另外三个值得早点学会:// 把结果向下取整为整数,% 给你余数,** 做幂运算。我用 % 的频率比我当初想的要高得多。
Juno各种运算符 四个基本运算符表现如预期,但有一条规则要记住:/ 永远返回 float。另外三个是 //(整除)、%(取余)、**(幂运算)。每一个都能替你省下写辅助函数的功夫。
Juno各种运算符 每个运算符都对应一个双下划线方法,所以一个类只要定义了 __add__ 或 __mul__,就能直接接入 + 和 *。混用 int 和 float 总是会扩展为 float,而 / 无论输入什么都返回 float。

除法:/ 与 // 的区别 ​

/ 永远给你精确的小数结果,即使答案是整数也一样。// 会去掉小数部分,并朝负无穷方向取整。对于正数来说,这就相当于砍掉小数;但对于负数来说,结果会比直接砍掉小数再多走一步,离零更远:

/ 永远返回 float,不管输入是不是整数。// 返回的是结果向下取整后的值:即小于或等于真实结果的最大整数。对正数来说这跟直接截断是一样的效果,但对负数就不一样了:

/ 是真除法,永远返回 float。// 是向下取整除法:它把真实的商朝负无穷方向取整,而不是像许多其他语言那样朝零方向截断。这在实践中的意义在于:Python 的 // 和 % 对任意整数输入(包括负数)都满足 a == (a // b) * b + (a % b) 这个等式。而截断除法在负数情况下会破坏这个等式,所以你写的任何循环或环绕逻辑在这里都能保持正确,不需要针对符号做特殊处理。

python
10 / 2     # 5.0   (永远是浮点数,即使能整除)
10 / 3     # 3.3333333333333335

10 // 3    # 3
7 // 2     # 3
-7 // 2    # -4    (朝负无穷方向取整,不是朝零方向)

大多数情况下你用 // 处理的都是正数。等负数出现时,记住上面的这个行为就好。

Juno除法:/ 与 // 的区别/ 永远返回小数,即使能整除也一样:4 / 2 是 2.0。// 会去掉小数部分,但要留意负数:-7 // 2 是 -4,不是 -3,因为它是向下取整而不是直接砍掉小数。我第一次遇到的时候就被坑了。

Python 把这个操作叫做向下取整除法,因为它用的是数学上的向下取整函数。其他语言通常是朝零方向截断,所以负数的结果会不一样。名字 // 本身就是个提示:先除法,再向下取整。

Juno除法:/ 与 // 的区别/ 永远返回 float,就连 4 / 2 也会给出 2.0。// 是向下取整而不是截断,所以 -7 // 2 是 -4。对正数来说两种方式看起来一样,差异只在负数上才会显现。

// 计算的是 floor(a / b),而不是截断。它对浮点数同样适用:7.5 // 2 是 3.0,取整后的商以 float 的形式返回,而不是 int。

Juno除法:/ 与 // 的区别// 对商做向下取整,所以它对任意输入(包括负数)都满足 a == (a // b) * b + (a % b)。这是写循环逻辑时可以依赖的性质。另外要注意,如果操作数中有 float,// 也会返回 float,比如 7.5 // 2 给出的是 3.0。

取余运算符 % ​

% 给你的是整除之后剩下的部分。如果 10 // 3 是 3(因为 3 能装进 10 里三次),那么 10 % 3 就是 1(因为 3 × 3 = 9,10 - 9 = 1)。最常见的用法是判断一个数是奇数还是偶数。

% 是取模运算符。判断奇偶是最常见的用法,但它可以推广到任何循环或环绕问题:把计数器控制在某个范围内、把项目分配到不同分组、重复一个序列。模式总是 value % limit,返回值介于 0 到 limit - 1 之间。

Python 的 % 是真正的数学模运算:结果的符号永远跟除数一致。许多其他语言取的是被除数的符号,所以对 -7 % 3,它们会给出 -1,而 Python 给出的是 2。Python 的这个结果,源自把模运算定义为 a - (a // b) * b,而 // 是朝负无穷方向取整。这样带来的好处是:即使 value 是负数,value % limit 也始终落在 0 到 limit - 1 之间,所以一个绕过列表末尾的索引不需要额外的判断就能落在合法范围内。

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

6 % 2     # 0  (偶数)
7 % 2     # 1  (奇数)
Juno取余运算符 %% 是整除之后剩下的部分。10 % 3 是 1,因为 3 能装进 10 里三次,还剩 1。你最常遇到它的场景是判断奇偶:n % 2 为 0 表示偶数,为 1 表示奇数。
Juno取余运算符 %% 是取模运算,用途远不止判断奇偶:任何时候你需要把一个值限制在某个范围内,value % limit 都能把它环绕到 0 到 limit - 1 之间。计数器、分组分配、循环重复,本质都是同一个模式。
Juno取余运算符 %% 的符号跟随除数,所以 -7 % 3 是 2,不是 -1。正因为这样,value % limit 即使输入是负数也能保持在范围内,环绕索引不需要额外的判断。

幂运算 ** ​

** 把一个数提升到某个次方。要用两个星号,不要用 ^ 符号(在 Python 中它另有含义):

** 是幂运算符。它也支持浮点数,这让你可以用小数次方来表示开根号,而不必额外调用一个函数:

** 做幂运算,而小数次方的形式很方便:n ** 0.5 不用导入任何东西就能算出平方根。两个 int 操作数会得到 int,只要有一个 float 操作数就会得到 float。在用它处理大数或精度要求高的计算之前有一点要知道:9 ** 0.5 走的是浮点数运算,所以某些输入可能会得到 2.9999999999999996 这样的结果。当精确性很重要时,math.isqrt()(用于整数开根号)和 math.pow() 是值得权衡使用的工具,在模块这一章会讲到。

python
2 ** 10    # 1024
3 ** 3     # 27
9 ** 0.5   # 3.0  (平方根:提升到 0.5 次方)
Juno幂运算 **** 把一个数提升到某个次方,所以 2 ** 10 是 1024。小数次方能给你开根号:9 ** 0.5 是 3.0。唯一的坑在符号上,要用 **,不要用 ^,后者在 Python 里做的是完全不同的事。
Juno幂运算 **** 是幂运算,而且支持浮点数,所以 9 ** 0.5 直接就能算出平方根,不需要额外调用函数。想在一行代码里快速开根号,就用小数次方的写法。
Juno幂运算 ** 两个整数运算得到整数,只要有一个浮点数就得到浮点数。用 ** 0.5 开根号的坑在于它走的是浮点数运算,可能会产生误差,所以精确性重要时要用 math.isqrt() 或 math.pow()。

运算符优先级 ​

Python 遵循标准的数学运算顺序:先算幂运算,再算乘除,最后算加减。如果你不确定,就用括号。括号能让意图一目了然,而且不需要付出任何代价:

Python 遵循标准的 PEMDAS/BODMAS 运算顺序。容易让人绊倒的地方在于:/、// 和 % 的优先级完全相同,混用时从左到右依次计算。括号不花钱,只要顺序不是一眼就能看明白,就用括号:

算术运算符的优先级从高到低依次是:**,然后是一元 -(写在单个值前面的负号),然后是 *、/、//、%(优先级相同时从左到右计算),最后是 +、-。在实际代码中会咬人的一个坑是:-2 ** 2 等于 -(2 ** 2),也就是 -4,因为 ** 的绑定比前面的负号更紧。如果你要对一个可能是负数的值求平方,一定要写成 (-2) ** 2 这样带括号的形式,否则符号会出错,而且不会有任何报错提醒你。

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
Juno运算符优先级 跟纸上算数的顺序一样:先算幂,再算乘除,最后算加减。如果一眼看不出顺序,就加上括号。括号不花钱,还能把你的意图说得明明白白。
Juno运算符优先级 标准的 PEMDAS 顺序,但有一个坑:/、// 和 % 优先级相同,混用时从左到右计算。这条从左到右的规则正是大家容易理解错的地方,所以分组不清楚时就加括号。
Juno运算符优先级 坑在于一元负号的绑定比 ** 更松,所以 -2 ** 2 是 -4。给可能是负数的东西求平方时写成 (-2) ** 2,否则符号会翻转,而且没有任何报错提醒你。

int 和 float 如何互相影响 ​

Python 有一条一致的规则:/ 永远返回小数,即使 4 / 2 也会给出 2.0。 只要一次运算中混入了整数和小数,结果也会是小数。如果你需要整数,就用 //,或者用 int() 转换。

类型规则是可预测的:/ 永远返回 float。两个整数做 // 和 % 运算返回 int。任何混合了 int 和 float 的运算都返回 float。这意味着 4 / 2 是 2.0,不是 2,而这在你需要一个整数时(比如用作索引)就很重要。

这条规则是固定的,值得牢记:只要两者混用,int 就会扩展为 float;/ 永远返回 float;只有当两个操作数都是 int 时,// 才返回 int。这条规则要防止的问题是:一个 float 悄悄混进了需要整数的地方,比如列表索引或字典的键,而在这些场合 2.0 和 2 并不能互换。当一次计算可能产生 float 而你需要的是 int 时,在那个环节用 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)
Junoint 和 float 如何互相影响/ 永远给出小数,所以 4 / 2 是 2.0,不是 2。把整数和小数混在一起做任何运算,出来的都是小数。当你需要整数时,用 //,或者用 int() 包一层。
Junoint 和 float 如何互相影响/ 永远是 float,两个整数做 // 和 % 保持 int,任何 int 和 float 混合都会变成 float。最容易踩坑的就是 4 / 2 是 2.0,但你其实想要的是一个索引。
Junoint 和 float 如何互相影响 从 / 里冒出来的一个意外的 float,会破坏任何需要精确 int 的地方,比如索引或字典的键,2.0 并不等同于 2。在那个边界上主动用 int() 转换,而不是指望除法结果恰好是整数。

浮点数精度 ​

有一个几乎每个人早晚都会撞上的坑:

python
0.1 + 0.2   # 0.30000000000000004

这个微小的误差不是 Python 的 bug。计算机用二进制存储小数,而像 0.1 这样的值没法精确表示。这跟 1/3 没法用小数精确写出来是同一个道理。对大多数日常计算来说这无关紧要。要展示金额的话,用 round() 或者 :.2f 格式说明符就能让输出显得整齐。

浮点数大致能提供 15 到 16 位有效十进制数字的精度。这种不精确来自某些小数没法在二进制中精确存储,所以 0.1 + 0.2 会产生 0.30000000000000004。这种偏差只有在你查看原始值时才会显现;用 :.2f 或 round() 格式化输出就能把它藏起来。

对于分厘累加很敏感的财务计算,Python 标准库提供了 decimal.Decimal,可以做精确的十进制运算。这个在模块这一章会讲到。

float 用二进制(以 2 为基数)存储数字,任何分母不是 2 的幂的分数(比如 1/10)都没有精确的二进制表示,就像 1/3 没有精确的十进制表示一样。这正是 0.1 + 0.2 会得到 0.30000000000000004 的根本原因。单次运算的误差很小,但在一长串算术运算中会不断累积,所以计算同一个总数的两种不同方式,最后几位数字可能会不一致。

避免这个坑的规则是:永远不要用 float 表示金额,也永远不要用 == 精确比较两个浮点数。当你需要精确的十进制运算(比如账单、会计)时,用 decimal.Decimal;当你需要完全没有舍入误差的精确比例时,用 fractions.Fraction。两者都随 Python 标准库自带,在模块这一章会讲到。

Juno浮点数精度 计算机用二进制存储小数,像 0.1 这样的值没有精确的二进制形式,所以 0.1 + 0.2 会得到 0.30000000000000004。这不是 Python 的 bug,每种语言都是这样。日常输出的话,用 round() 或 :.2f 就能让它显得整齐。
Juno浮点数精度 二进制没法精确存储每一个小数,所以 0.1 + 0.2 会偏移成 0.30000000000000004。大多数运算场景下这无所谓,:.2f 或 round() 能让显示结果变整齐。当分厘必须对上时,就换成 decimal.Decimal。
Juno浮点数精度 能救你的规则是:不要用浮点数表示金额,也永远不要对浮点数用 ==,因为误差会随着一系列运算不断累积。需要精确十进制时用 decimal.Decimal,需要精确比例时用 fractions.Fraction。

更易读的数字写法 ​

Python 允许你在数字字面量中插入下划线,让大数字更容易读。Python 会完全忽略这些下划线,它们只是为了方便你看:

下划线可以出现在数字字面量的任何位置,在解析时会被去掉,对值没有任何影响。这对常量中的千位分隔符很有用,对二进制或十六进制字面量中给数字分组也一样有用:

下划线在 Python 看到这个值之前就已经被去掉了,所以对值完全没有影响。它在整数、浮点数以及带基数前缀的字面量中都能用(0xFF_FF、0b1010_0001、1_234.567_890),这不仅对十进制的千位分隔有用,对十六进制常量中给字节分组也很方便。唯一的限制是:下划线不能出现在开头、末尾,也不能紧挨着小数点或指数标记。

python
population = 8_100_000_000
distance_km = 384_400
pi_approx = 3.141_592_653
Juno更易读的数字写法 在长数字里插入下划线让它更好读:8_100_000_000 跟 8100000000 是同一个值。Python 会完全忽略这些下划线,它们是给你的眼睛看的,不是给解释器看的。
Juno更易读的数字写法 下划线可以出现在数字字面量的任何位置,解析时就会消失,所以值不受影响。对常量中的千位分隔符很好用,对二进制或十六进制中给数字分组也同样好用。
Juno更易读的数字写法 下划线在整数、浮点数和带基数前缀的字面量里都只是装饰性的,所以 0xFF_FF 和 0b1010_0001 看起来更清晰。唯一的规则是:不能放在开头、末尾,也不能紧贴小数点或指数标记。

有用的内置函数 ​

abs() ​

abs() 返回绝对值:结果永远是正数,跟输入的符号无关。当你关心一个数离零有多远、而不关心方向时,就用它。

abs() 返回一个数的大小(去掉符号后的值)。整数和浮点数都能用。适合用在距离计算、误差范围判断,以及任何方向无关、只需要知道数值大小的场景。

abs() 返回一个数的大小,返回类型跟输入类型一致:传入 int 得到 int,传入 float 得到 float。日常用法是比较两个值离某个目标有多远,而不关心它们落在哪一侧,比如用 abs(measured - expected) < tolerance 做一个容差检查,这比对一个带符号的范围做两次比较读起来更清楚。

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5
Junoabs()abs() 去掉符号,只给你正数大小:abs(-5) 是 5,abs(3.7) 是 3.7。当你只关心一个数离零有多远、不关心它偏向哪个方向时就用它。
Junoabs()abs() 给出 int 或 float 的大小,方向被丢弃。写距离检查和误差范围检查时,只要符号不是问题的一部分,用它写出来会很干净。
Junoabs()abs() 保留输入的类型,int 进去还是 int 出来。值得记住的模式是 abs(measured - expected) < tolerance:一次容差检查代替对带符号范围做两次比较。

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() 使用的是四舍六入五取偶(银行家舍入法):恰好处于中间的值会舍入到最接近的偶数整数,而不是永远向上舍入。这样能防止舍入误差在一长列数字中偏向同一个方向累积,这也是为什么它是统计工作中的默认舍入方式。有两点实践中要留意的地方。第一,float 的"正中间"在转成二进制后往往不是真正的正中间,所以 round(2.675, 2) 给出的是 2.67,不是 2.68,因为 2.675 没法被精确存储。第二,当你需要可预测的货币舍入时,应该用 decimal.Decimal 配合明确指定的舍入模式来做,而不是依赖对浮点数使用 round()。

python
round(2.5)   # 2
round(3.5)   # 4
round(4.5)   # 4
Junoround()round(x) 舍入到最接近的整数,round(x, n) 保留 n 位小数。意外之处在于:恰好处在正中间的值会舍入到最接近的偶数,所以 round(2.5) 是 2,不是 3。
Junoround()round() 用的是银行家舍入法,正中间的值会舍入到最接近的偶数:round(2.5) 是 2,round(4.5) 是 4。这在汇总统计中能减少偏差,但如果你以为正中间总是向上舍入,就要注意这一点。
Junoround() 银行家舍入法加上二进制存储,意味着 round(2.675, 2) 会得到 2.67,因为 2.675 在底层根本不是一个精确的正中间值。要处理货币金额,就用 decimal.Decimal 配合明确选定的舍入模式,别指望对浮点数用 round()。

divmod() ​

divmod() 在一次调用里同时给你商和余数。它返回一对值,也就是一个元组(在元组和集合这一章会讲到),你可以一次性把它赋值给两个变量:

divmod(a, b) 等价于 (a // b, a % b),但只用一步就计算出来。当你反正两个值都需要用到时就用它:分页、时间转换,或者把项目分配到不同分组。

divmod(a, b) 只做一次除法运算,同时把向下取整的商和取模的余数一起以一对值的形式返回,这样你就不用先用 // 再用 % 重复计算同一个除法。在你反正两个结果都需要用到的地方,它读起来最干净:时间转换(divmod(seconds, 60))、分页,或者把项目排布到固定大小的行里。

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
Junodivmod()divmod(a, b) 把商和余数一起给你:divmod(10, 3) 是 (3, 1)。你可以用 quotient, remainder = divmod(10, 3) 一次性把两者都拆出来。
Junodivmod()divmod(a, b) 一步就相当于 (a // b, a % b)。当你两个结果都需要用到时就用它,比如时间转换或分页,而不用把除法算两遍。
Junodivmod() 一次除法,两个结果:divmod() 省掉了分别用 // 和 % 重复计算的工作。在你反正两个结果都要用的地方最好用,比如 divmod(seconds, 60) 或者行布局计算。

实战演练 ​

一个小费计算器:

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

print(f"账单:${bill}")
print(f"小费:${tip}")
print(f"总计:${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},剩余:{leftover}")
print(f"所需总页数:{total_pages}")   # 16
python
total_files = 847
processed_files = 312

percent = round(processed_files / total_files * 100, 1)
print(f"进度:{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:.2f}")   # 0.75

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

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

这两种模式最终都归结为一个比值:一个值相对于某个参考范围或参考量级的比值。对大多数分析工作来说,float 的精度已经足够;只有当计算链涉及几十次运算,或者涉及数量级相差极大的数值时,累积误差才会真正成为问题。