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__,等等。实际的好处是你自己的类可以定义这些方法,然后直接用 +*,就像内置数字一样。intfloat 混合在一个运算里总是返回 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__ 的类直接接入 +*intfloat 混合总是扩大到 float/ 不管你给什么都返回 float

除法:/ vs //

/ 总是给你精确的小数结果,即使答案是整数。// 丢弃小数部分,朝负无穷方向舍入。对正数来说就是去掉小数,但对负数来说会更远离零:

/ 总是返回 float,不管输入是否是整数。// 返回结果的下整数:小于等于真实结果的最大整数。对正数来说和截断一样。对负数来说不一样:

/ 是真正的除法,总是返回 float// 是整除:它朝负无穷方向舍入真正的商,而不是很多其他语言做的朝零截断。这在实际中很关键:Python 的 //% 满足对每个整数输入都有 a == (a // b) * b + (a % b),包括负数。截断除法对负数会破坏这个恒等式,所以你写的任何循环或折返逻辑在这里都能正确工作,不用特殊处理符号。

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

10 // 3    # 3
7 // 2     # 3
-7 // 2    # -4    (朝负无穷舍入,不是朝零)

你大部分时候会对正数用 //。负数出现时记住这个行为。

Juno除法:/ vs /// 总是给回小数,即使整除:4 / 22.0// 丢弃小数部分,但看负数:-7 // 2-4,不是 -3,因为它朝负无穷舍入而不是截断。第一次做的时候被坑过。

Python 把这叫做整除,因为它应用了数学上的下取整函数。其他语言朝零截断,对负数就会给出不同的结果。符号 // 暗示这一点:除,然后取整。

Juno除法:/ vs /// 总是返回 float,即使 4 / 2 也给 2.0// 朝负无穷舍入而不是截断,所以 -7 // 2-4。对正数两个看起来一样,差异只在负数上出现。

// 计算 floor(a / b),不是截断。它也对浮点数工作:7.5 // 23.0,舍入的商以 float 而不是 int 返回。

Juno除法:/ vs //// 舍入商,所以对每个输入都满足 a == (a // b) * b + (a % b),包括负数。这是用于循环逻辑要依靠的性质。注意 float 操作数使 // 返回 float,比如 7.5 // 23.0

求余运算符 %

% 给你整除后剩下的东西。如果 10 // 33(因为 3 进入 10 三次),那么 10 % 31(因为 3 × 3 = 9,10 - 9 = 1)。最常见的用法是检查一个数是奇数还是偶数。

% 是求余运算符。检查奇偶是最常见的用途,但它泛化到任何循环或折返问题:保持计数器在某个范围内、在组间分配项目、重复序列。模式总是 value % limit,返回 0limit - 1 之间的东西。

Python 的 % 是真正的求余:结果总是跟除数的符号一致。很多其他语言取被除数的符号,所以它们会对 -7 % 3-1,而 Python 给 2。Python 的答案来自定义求余为 a - (a // b) * b,其中 // 朝负无穷舍入。好处是 value % limit 即使 value 是负数也保持在 0limit - 1 范围内,所以一个绕过列表末端的索引着陆在范围内,不需要防护子句。

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

6 % 2     # 0  (偶数)
7 % 2     # 1  (奇数)
Juno求余运算符 %% 是整除后的余数。10 % 31,因为 3 进入 10 三次,余 1。你最常见到它用作奇偶检查:n % 2 对偶数是 0,对奇数是 1
Juno求余运算符 %% 是求余,远不止奇偶:任何时候你需要保持值在一个范围内,value % limit 就把它折返到 0limit - 1。计数器、组分配、循环重复,全都是这个形状。
Juno求余运算符 %% 遵循除数的符号,所以 -7 % 32,不是 -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 ** 101024。分数幂给你根:9 ** 0.53.0。唯一的陷阱是符号,用 **,不是 ^,那在 Python 里做别的东西。
Juno乘方 **** 是乘方,它取浮点数,所以 9 ** 0.5 给你平方根,不用单独的函数调用。当你想快速内联一个根时,用分数幂形式。
Juno乘方 ** 两个 int 给 int,任何 float 给 float。** 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,不然符号翻转而且没错误标记。

intfloat 如何交互

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

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

规则是固定的,值得内化:int 任何时候和 float 混合就扩大到 float/ 总是返回 float// 只有两个操作数都是 int 时才返回 int。这防止的 bug 是 float 偷偷进入需要整数的东西,比如列表索引或字典键,2.02 在那里不可互换。当计算可能产生 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 / 22.0,不是 2。任何地方混合整数和小数,你得到小数。需要整数时,用 // 或包在 int() 里。
Junoint 和 float 如何交互/ 总是 float//% 在两个 int 上保持 int,任何 intfloat 混合出来 float。那个抓住人的是 4 / 22.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。漂移只在你检查原始值时出现;用 :.2fround() 格式化在输出里隐藏它。

对于分数累积的金融工作,Python 在标准库里提供 decimal.Decimal,有精确的十进制算术。那在模块章节讲。

float 用二进制(基 2)存储数字,任何分母不是 2 的幂的分数(比如 1/10)没有精确的二进制形式,和 1/3 没有精确的十进制形式一样。这就是 0.1 + 0.2 着陆在 0.30000000000000004 的整个原因。每次运算的误差很小,但跨长串算术累积,所以两种计算同一总和的方式可以在最后几位数字上不同。

阻止 bug 的规则:永远不要用 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。对大多数数学很好,:.2fround() 整洁显示。分币必须加起来就切换到 decimal.Decimal
Juno浮点数精度 救你的规则:钱没浮点数,永远不要在浮点数上 ==,因为误差跨操作链累积。用 decimal.Decimal 作精确十进制和 fractions.Fraction 作精确比率。

可读的数字字面量

Python 让你在数字字面量里放下划线来让大数字更可读。Python 完全忽略它们;它们是给你看的:

下划线在数字字面量任何地方都有效,解析时被去掉没有对值的影响。对常量里的千位分隔符很有用,对二进制或十六进制字面量里的数字分组:

下划线在 Python 看值之前被去掉,所以它们对值零影响。它们在整数、浮点数和基数字面量里都工作(0xFF_FF0b1010_00011_234.567_890),这让它们不仅对十进制的千位分组有用,对十六进制常量里的字节分组也有用。唯一的限制:下划线不能在开始、结束,或靠近小数点或指数标记。

python
population = 8_100_000_000
distance_km = 384_400
pi_approx = 3.141_592_653
Juno可读的数字字面量 在长数字里放下划线来让它可读:8_100_000_0008100000000 是同一值。Python 完全忽略它们,它们是给你的眼睛,不是解释器。
Juno可读的数字字面量 下划线在数字字面量任何地方都有效,解析时消失,所以值不变。对常量里的千位分隔符很好,对二进制或十六进制的数字分组一样有用。
Juno可读的数字字面量 下划线跨整数、浮点数和基数字面量是装饰性的,所以 0xFF_FF0b1010_0001 读得更干净。唯一的规则:不在开始、结束,或靠近小数点或指数标记。

有用的内置函数

abs()

abs() 返回绝对值:总是正的,不管输入的符号。当你关心一个数离零多远,不是哪个方向时用它。

abs() 返回一个数的大小。对整数和浮点数工作。对距离计算、误差边界,和任何方向无关只需值大小的情况有用。

abs() 返回一个数的大小,返回类型跟输入匹配:intint 出,floatfloat。日常用途是比较两个值离目标多远,不在乎它们落在哪一边,比如 abs(measured - expected) < tolerance 作容差检查,读起来比两个围绕有符号范围的比较清楚。

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5
Junoabs()abs() 剥掉符号,给回正的大小:abs(-5)5abs(3.7)3.7。关心一个数离零多远,不是方向时用它。
Junoabs()abs()intfloat 的大小,方向丢弃。这是写距离和误差边界检查的干净方式,任何符号不是问题的地方。
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)2round(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:  ${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 的精度对大多数分析工作足够;累积误差只在计算链几十个运算或涉及相差许多数量级的值时重要。