Python 里 / 和 // 有什么区别?负数取模 -7 % 3 为什么是 2?
简化版
Python 有两个除法运算符:/ 是「真除法」永远返回浮点数(6 / 3 得 2.0 不是 2),// 是「整除/地板除」返回向下取整的结果;而 % 是取模,配合 // 满足 a == (a // b) * b + a % b。关键点:① / 真除法——即使能整除也返回 float(4 / 2 == 2.0),这是 Python 3 的行为(Python 2 里 / 对整数是整除,是常见迁移坑);② // 地板除——向「负无穷」方向取整(floor),所以 7 // 2 == 3、-7 // 2 == -4(不是 -3!向下取整);③ % 取模——Python 的取模结果符号「跟除数(第二个操作数)走」,所以 -7 % 3 == 2(正)、7 % -3 == -2(负),这和 C/Java(结果跟被除数走,-7 % 3 == -1)不同。-7 % 3 == 2 的原因:-7 // 3 == -3(向下取整),则 -7 % 3 = -7 - (-3)*3 = -7 + 9 = 2。divmod(a, b) 一次返回 (a // b, a % b)。核心记忆:/ 永远浮点、// 向下取整(负数向更小取)、% 符号跟除数走(-7%3=2),三者满足 a=(a//b)*b + a%b。
详细版
三个运算的规则:
| 运算 | 名称 | 规则 | 例子 |
|---|---|---|---|
/ | 真除法 | 永远返回 float | 6/3 → 2.0,7/2 → 3.5 |
// | 地板除 | 向下(负无穷)取整 | 7//2 → 3,-7//2 → -4 |
% | 取模 | 结果符号跟除数走 | -7%3 → 2,7%-3 → -2 |
divmod(a,b) | 商和余数 | 返回 (a//b, a%b) | divmod(-7,3) → (-3,2) |
print(6 / 3) # 2.0(真除法,即使整除也是 float)
print(7 / 2) # 3.5
print(7 // 2) # 3(向下取整)
print(-7 // 2) # -4(向负无穷取整,不是 -3!)
print(7 // -2) # -4
print(7.0 // 2) # 3.0(有 float 参与,// 返回 float)
# 取模:符号跟除数(第二个操作数)
print(-7 % 3) # 2 (除数 3 正 → 结果正)
print(7 % -3) # -2 (除数 -3 负 → 结果负)
print(-7 % -3) # -1
# 恒等式:a == (a // b) * b + (a % b)
a, b = -7, 3
print((a // b) * b + (a % b)) # -3*3 + 2 = -9+2 = -7 ✓
# divmod 一次拿商和余数
print(divmod(-7, 3)) # (-3, 2)
⚠️ 抓住一条铁律就能推出所有结果:Python 的
//是「地板除」(向负无穷取整),而%由恒等式a == (a // b) * b + a % b反推出来——记住//向下取整、余数补齐恒等式,-7 % 3 = 2就是自然结果。很多人被-7 % 3 == 2搞懵,是因为 C/Java 的取模「跟被除数走」(-7 % 3 == -1,因为它们的整数除法是「向零截断」)。Python 选择了「向下取整 + 余数符号跟除数」的组合,好处是:取模结果永远和除数同号,对正除数(最常见)永远得到[0, b)的非负余数——这对「循环取索引」idx % n、「时钟/角度回绕」(h - 3) % 12、「哈希分桶」等场景极其方便,负数也能得到正确的非负下标,不用额外+n修正。理解这个设计意图,就不会觉得它「反直觉」了。
完整版教学
一、真除法 / 与地板除 //
先分清两个除法:
/ (真除法,true division):
永远返回浮点数 float
6 / 3 → 2.0(即使能整除,也是 2.0 不是 2)
7 / 2 → 3.5
→ Python 3 的行为:/ 总是"数学意义的除法",结果带小数
// (地板除,floor division):
返回"向下取整"的商
7 // 2 → 3(3.5 向下取 3)
结果类型:两个 int → int;有 float 参与 → float
7 // 2 → 3(int)
7.0 // 2 → 3.0(float,但值是整数)
Python 2 的坑(迁移常见):
Python 2 里 / 对两个整数是"整除":7 / 2 → 3(不是 3.5)
Python 3 改成真除法:7 / 2 → 3.5
→ Py2 迁 Py3 时,原来靠 / 做整除的代码会变浮点(要改成 //)
(from __future__ import division 可在 Py2 启用 Py3 行为)
选择:
要精确小数结果 → /
要整数商(如索引、页数、分组)→ //
所以 / 真除法永远返回 float、// 地板除向下取整;Py2 的 / 是整除是迁移坑
先分清两个除法:/(真除法)永远返回浮点数(6/3 → 2.0 即使整除也是 2.0、7/2 → 3.5,Python 3 的行为,总是数学意义的除法);//(地板除)返回向下取整的商(7//2 → 3,两个 int→int、有 float 参与→float,7.0//2 → 3.0)。Python 2 的坑(迁移常见):Py2 里 / 对两个整数是「整除」(7/2 → 3)、Py3 改成真除法(7/2 → 3.5),迁移时靠 / 做整除的代码会变浮点(要改成 //)。选择:精确小数用 /、整数商(索引/页数/分组)用 //。理解「/ 真除法永远返回 float(即使整除)、// 地板除向下取整(int/int→int,有 float→float);Py2 的/是整除是迁移坑;小数用/、整数商用//」,就掌握了两个除法。
二、地板除的「向下取整」——负数的关键
理解 // 对负数的行为:
// 是"地板除"(floor division):
向"负无穷"方向取整(数学 floor 函数)
→ 不是"向零截断"(C/Java 那样)
正数没差别:
7 // 2 = 3(3.5 向下取 3,向零截断也是 3)
负数才见分晓:
-7 // 2 = ?
数学结果 -3.5
向下取整(向负无穷)→ -4 ✓ Python
向零截断 → -3 ✗(C/Java 的整数除法)
→ Python 的 -7 // 2 = -4(比 -3.5 更小的整数)
7 // -2 = ?
数学 -3.5 → 向下取整 → -4
图示(数轴上"向下"= 向左 = 向负无穷):
-4 -3.5 -3
|------●------|
↓ floor 取左边的 -4
对比截断(trunc,向零):
trunc(-3.5) = -3(向右,靠近 0)
floor(-3.5) = -4(向左,远离 0)
→ Python // 用 floor,C/Java 整数除用 trunc
要"向零截断"的整除:
用 int(a / b) 或 math.trunc(a / b)
int(-7 / 2) = int(-3.5) = -3(向零)
所以 // 向负无穷取整(floor):-7//2=-4(不是-3);要向零截断用 int(a/b)
// 是「地板除」,向「负无穷」方向取整(数学 floor),不是「向零截断」。正数没差别(7//2=3);负数才见分晓:-7//2 数学结果 -3.5、向下取整(向负无穷)→ -4(Python),而向零截断→ -3(C/Java);7//-2 同样是 -4。图示:数轴上「向下=向左=向负无穷」,floor(-3.5) 取左边的 -4。对比截断:trunc(-3.5)=-3(向零)、floor(-3.5)=-4(向负无穷)。要「向零截断」的整除:用 int(a/b) 或 math.trunc(a/b)(int(-7/2)=int(-3.5)=-3)。理解「// 向负无穷取整(floor):-7//2=-4 不是-3(正数无差别、负数见分晓);floor 向左远离 0、trunc 向零;要向零截断用 int(a/b)」,就掌握了地板除的负数行为。
三、取模 % 与恒等式
理解取模和它与地板除的关系:
取模的恒等式(一切的根源):
a == (a // b) * b + (a % b)
→ 余数 = a - (a // b) * b
→ 因为 // 是地板除,余数由它反推
Python 取模的特点:结果符号"跟除数(b)走"
-7 % 3:
-7 // 3 = -3(向下取整)
-7 % 3 = -7 - (-3)*3 = -7 + 9 = 2 ✓(正,跟除数 3)
7 % -3:
7 // -3 = -3(向下取整,2.33→ -3? 不,7/-3=-2.33 向下取 -3)
7 % -3 = 7 - (-3)*(-3) = 7 - 9 = -2 ✓(负,跟除数 -3)
-7 % -3:
-7 // -3 = 2(-7/-3=2.33 向下取 2)
-7 % -3 = -7 - 2*(-3) = -7 + 6 = -1(负,跟除数)
规律:
除数 b > 0 → 余数在 [0, b)(非负)
除数 b < 0 → 余数在 (b, 0](非正)
→ 余数符号总和除数一致
对比 C/Java(取模跟被除数走):
C: -7 % 3 = -1(因为 C 整数除法向零截断:-7/3=-2,
-7 - (-2)*3 = -7+6 = -1)
→ 语言设计不同:Python floor+余数跟除数、C trunc+余数跟被除数
所以取模由恒等式 a=(a//b)*b+a%b 推出,Python 余数符号跟除数走
取模的恒等式(一切的根源):a == (a // b) * b + (a % b),余数 = a - (a // b) * b,因为 // 是地板除、余数由它反推。Python 取模的特点:结果符号「跟除数(b)走」:-7 % 3——-7//3=-3、-7%3 = -7-(-3)*3 = -7+9 = 2(正,跟除数 3);7 % -3 = -2(负,跟除数 -3);-7 % -3 = -1。规律:除数 b>0 → 余数在 [0,b) 非负、除数 b<0 → 余数在 (b,0] 非正(余数符号总和除数一致)。对比 C/Java(取模跟被除数走):C 里 -7 % 3 = -1(整数除法向零截断)。理解「取模恒等式 a=(a//b)*b+a%b、余数由地板除反推;Python 余数符号跟除数走(-7%3=2 正、b>0 余数在[0,b));C/Java 跟被除数走(-7%3=-1)」,就掌握了取模。
四、-7 % 3 == 2 的完整推演
把 -7 % 3 == 2 一步步算清楚:
求 -7 % 3:
第一步:算 -7 // 3(地板除,向下取整)
-7 / 3 = -2.333...
向下取整(向负无穷)→ -3
(注意不是 -2!-2.333 向下是更小的 -3)
所以 -7 // 3 = -3
第二步:用恒等式 a = (a//b)*b + a%b 求余数
-7 = (-7 // 3) * 3 + (-7 % 3)
-7 = (-3) * 3 + r
-7 = -9 + r
r = -7 + 9 = 2
所以 -7 % 3 = 2 ✓
验证符号规律:
除数 3 > 0 → 余数应在 [0, 3) → 2 ✓(在范围内)
对比 C 语言(会得 -1):
C 整数除法向零截断:-7 / 3 = -2(截断 -2.333)
C: -7 % 3 = -7 - (-2)*3 = -7 + 6 = -1
→ 差别全在"除法怎么取整"(floor vs trunc)
一句话推法:
记 // 向下取整 + 恒等式补余数
→ -7//3=-3 → 余数=−7−(−9)=2
所以-7%3=2:因-7//3=-3(向下取整),恒等式补出余数-7-(-9)=2
把 -7 % 3 == 2 一步步算清:第一步算 -7 // 3(地板除向下取整):-7/3 = -2.333、向下取整(向负无穷)→ -3(不是 -2,-2.333 向下是更小的 -3);第二步用恒等式求余数:-7 = (-3)*3 + r → -7 = -9 + r → r = 2,所以 -7 % 3 = 2。验证符号规律:除数 3>0 → 余数应在 [0,3) → 2 在范围内。对比 C(会得 -1):C 整数除法向零截断 -7/3=-2、-7%3 = -7-(-2)*3 = -1——差别全在「除法怎么取整」(floor vs trunc)。理解「-7%3=2 推演:①-7//3=-3(向下取整,-2.333 向下是-3)②恒等式-7=(-3)*3+r→r=2;C 里向零截断-7/3=-2 得余数-1;差别在 floor vs trunc」,就彻底掌握了这个经典问题。
五、为什么 Python 这样设计——实用价值
理解 floor 取模的设计意图:
Python 选 floor 除法 + 余数跟除数的好处:
核心好处:对正除数,余数永远非负 [0, b)
→ 负数也能得到正确的"非负下标/回绕值"
场景1:循环索引/环形缓冲
n = 5
idx = (i - 2) % n # 无论 i 多小,结果都在 [0,5)
i=0: (0-2)%5 = -2%5 = 3 ✓(正确回绕到 3)
→ C 里 -2 % 5 = -2(负下标,越界!要手动 +5)
场景2:时钟/角度回绕
hour = (current - 3) % 12 # 往前 3 小时
(1 - 3) % 12 = -2 % 12 = 10 ✓(正确)
→ 直接得正确的小时数,不用判断负数
场景3:哈希分桶
bucket = hash(key) % num_buckets
# hash 可能为负,Python 的 % 保证 bucket 在 [0, num_buckets)
→ 直接当数组下标,安全
场景4:判断奇偶/整除(正除数时和 C 一致)
n % 2 == 0 # 偶数(n 正负都对,因为 % 2 结果非负)
对比 C 的痛:
C 里负数取模得负,做下标/回绕要额外 ((x % n) + n) % n 修正
→ Python 的设计省掉这个修正,更符合"取模用于回绕"的意图
所以 floor 取模好处:正除数余数非负,负数也能直接当下标/回绕值,省+n 修正
Python 选 floor 除法 + 余数跟除数 的好处:核心——对正除数,余数永远非负 [0, b),负数也能得到正确的「非负下标/回绕值」。场景1 循环索引/环形缓冲:(i-2) % n 无论 i 多小结果都在 [0,n)(-2%5=3 正确回绕,C 里 -2%5=-2 负下标越界要手动 +5);场景2 时钟/角度回绕:(current-3) % 12 直接得正确小时数;场景3 哈希分桶:hash(key) % num_buckets 保证在 [0,n) 直接当下标;场景4 判奇偶:n % 2 == 0(n 正负都对)。对比 C 的痛:C 里负数取模得负、做下标/回绕要 ((x%n)+n)%n 修正,Python 省掉这个修正。理解「floor 取模好处:正除数余数非负[0,b),负数也能直接当下标/回绕值;场景循环索引/时钟回绕/哈希分桶/判奇偶;C 里负取模得负要+n 修正,Python 省掉」,就理解了设计意图。
六、divmod 与实践总结
理解 divmod 和实践要点:
divmod(a, b):一次返回 (商, 余数) = (a // b, a % b)
divmod(17, 5) → (3, 2) (17 = 3*5 + 2)
divmod(-7, 3) → (-3, 2)
好处:
① 一次算出商和余数(比分别 // 和 % 高效,只除一次)
② 常用于进制转换、时间换算、分页
例:秒转 时:分:秒
m, s = divmod(total_seconds, 60)
h, m = divmod(m, 60)
→ (h, m, s)
例:分页
pages, remainder = divmod(total, per_page)
if remainder: pages += 1 # 有余数多一页
实践速查:
精确小数 → /(返回 float)
整数商 → //(向下取整)
余数/回绕 → %(符号跟除数)
商和余数 → divmod
向零截断整除 → int(a/b) 或 math.trunc
向上取整 → -(-a // b) 或 math.ceil(a/b)
浮点除法注意:
0.1 + 0.2 != 0.3(浮点精度),% 对浮点也有精度问题
→ 精确小数用 decimal.Decimal / fractions.Fraction
核心总结:
/ 永远浮点、// 向下取整(负数向更小)、% 符号跟除数
恒等式 a=(a//b)*b+a%b、-7%3=2、divmod 一次拿商余
floor 取模让负数能直接做下标/回绕(省 +n 修正)
所以 divmod 一次拿商余(时间/分页常用),按需选 / // % divmod
divmod(a, b) 一次返回 (商, 余数) = (a//b, a%b)(divmod(17,5)→(3,2)、divmod(-7,3)→(-3,2))。好处:① 一次算出商和余数(只除一次、比分别算高效)、② 常用于进制转换/时间换算/分页(秒转时分秒 m,s=divmod(sec,60); h,m=divmod(m,60);分页 pages,rem=divmod(total,per_page))。实践速查:精确小数用 /、整数商用 //、余数/回绕用 %、商和余数用 divmod、向零截断用 int(a/b)、向上取整用 -(-a//b) 或 math.ceil。浮点注意:0.1+0.2!=0.3(精度)、精确小数用 Decimal/Fraction。理解「divmod 一次拿商余(时间换算/分页常用只除一次);速查小数/、整数商//、回绕%、商余 divmod、向零 int(a/b)、向上-(-a//b);浮点有精度问题用 Decimal」,就掌握了 divmod 与实践。
记忆钩子:「Python 三个除法/取模运算:①/ 真除法永远返回 float(6/3=2.0 即使整除,Py2 里/是整除是迁移坑)②// 地板除向下取整(向负无穷 floor,不是向零截断):7//2=3、★-7//2=-4(不是-3!因-3.5 向下取更小的-4)③% 取模,结果符号跟除数(第二个操作数)走:-7%3=2(除数正→正)、7%-3=-2(除数负→负);根源恒等式 a==(a//b)*b+a%b,余数由地板除反推;★-7%3=2 推演:-7//3=-3(向下取整)→-7=(-3)*3+r→r=2(和 C/Java 不同,它们向零截断得-7%3=-1);Python 这样设计的好处:正除数余数永远非负[0,b),负数也能直接当下标/回绕值(idx%n、时钟(h-3)%12、哈希分桶),省掉 C 里的((x%n)+n)%n 修正;divmod(a,b)一次返回(a//b,a%b),时间换算/分页常用」。
七、常见误区与追问
- 误区:6 / 3 返回整数 2。 在 Python 3 里
/是真除法、永远返回浮点数,6 / 3得2.0而不是2(即使能整除);要得到整数商用//(6 // 3得2);注意 Python 2 里/对两个整数是整除(6/2得 3、7/2得 3),这是 Py2 迁 Py3 的常见坑——原来靠/做整除的代码在 Py3 会变成浮点,需要改成//。 - 误区:-7 // 2 等于 -3(向零截断)。 等于 -4——Python 的
//是「地板除」,向负无穷方向取整(floor),-7/2 = -3.5向下取整得更小的-4,而不是向零截断的-3;正数没有区别(7//2=3),只有负数参与时 floor 和 trunc 才不同;要「向零截断」的整除用int(a/b)或math.trunc(a/b)。 - 误区:Python 的 -7 % 3 应该等于 -1(像 C/Java 一样)。 Python 里等于 2——因为 Python 的取模结果符号「跟除数走」,除数 3 为正、余数就非负(落在 [0,3)),由恒等式
-7 = (-7//3)*3 + r = (-3)*3 + r反推出r=2;C/Java 的取模「跟被除数走」(它们整数除法向零截断),所以得 -1;两者都满足各自的「商×除数+余数=被除数」,只是取整方向不同。 - 误区:% 只能用于整数。 也能用于浮点数——
7.5 % 2 = 1.5、5.5 % 2.0 = 1.5;不过浮点取模会有浮点精度问题(如0.1 % 0.03可能不精确),需要精确结果时用decimal.Decimal或fractions.Fraction;另外divmod也支持浮点(divmod(7.5, 2)得(3.0, 1.5))。 - 追问:为什么 -7 % 3 是 2?请推导。 分两步:① 先算
-7 // 3,Python 的//是地板除、向负无穷取整,-7/3 = -2.333向下取整得-3(不是 -2);② 再用恒等式a = (a // b) * b + (a % b)反推余数:-7 = (-3) * 3 + r,即-7 = -9 + r,解出r = 2;所以-7 % 3 = 2;验证符号规律:除数 3 为正,余数落在 [0, 3) 内,2 符合;对比 C 语言因整数除法向零截断(-7/3=-2),得-7 % 3 = -7 - (-2)*3 = -1。 - 追问:Python 的取模为什么设计成「余数符号跟除数走」,有什么实际好处? 好处是「对正除数,余数永远非负、落在 [0, b) 区间」,这让负数也能得到正确的「非负下标/回绕值」,非常适合取模的常见用途——环形缓冲/循环索引
(i-k) % n(i 再小结果也在 [0,n) 内、能正确回绕)、时钟/角度回绕(hour-3) % 12、哈希分桶hash(key) % num_buckets(hash 为负时结果仍非负、可直接当数组下标);而 C/Java 里负数取模得负数,做这些回绕/下标运算要额外写((x % n) + n) % n来修正,Python 的设计省掉了这个修正、更贴合「取模=回绕」的语义意图。 - 追问:divmod 有什么用,比分别写 // 和 % 好在哪?
divmod(a, b)一次返回(a // b, a % b)元组,好处是:① 只做一次除法运算就同时得到商和余数(比分别写a // b和a % b少算一次,效率略高,尤其大整数时明显);② 语义清晰、代码简洁,特别适合「进制转换、时间换算、分页」这类同时需要商和余数的场景,比如秒转时分秒m, s = divmod(total, 60); h, m = divmod(m, 60)、分页pages, rem = divmod(total, per_page)(有余数则页数 +1);结果同样遵循地板除规则,divmod(-7, 3)得(-3, 2)。
八、加强记忆
Python 有两个除法运算符加一个取模,全由「地板除 + 恒等式」推出:① /(真除法)永远返回浮点数(6/3 = 2.0 即使整除也是 float;Python 2 里 / 对整数是整除、是迁移坑);② //(地板除)向「负无穷」方向取整(floor,不是向零截断):7//2 = 3、-7//2 = -4(不是 -3!-3.5 向下取更小的 -4);③ %(取模)结果符号「跟除数走」:-7 % 3 = 2(除数正→正)、7 % -3 = -2(除数负→负),这和 C/Java(跟被除数走、-7%3=-1)不同。根源是恒等式 a == (a // b) * b + (a % b)——余数由地板除反推。-7 % 3 = 2 推演:-7//3 = -3(向下取整)→ -7 = (-3)*3 + r → r = 2。Python 这样设计的好处:对正除数余数永远非负 [0, b),负数也能直接当下标/回绕值(循环索引 idx % n、时钟 (h-3) % 12、哈希分桶),省掉 C 里的 ((x%n)+n)%n 修正。divmod(a, b) 一次返回 (a//b, a%b)(时间换算、分页常用、只除一次)。一句话「**/ 永远 float、// 向下取整(负数向更小:-7//2=-4)、% 符号跟除数(-7%3=2);恒等式 a=(a//b)b+a%b;floor 取模让负数直接做下标/回绕(省+n 修正);divmod 一次拿商和余数*」。