🧠 第三章 编程语言
📄 一句话概括

7 和 2:+ 9、- 5、* 14、/ 3.5、// 3、% 1、幂 49。 四个坑:真除法出浮点、负数整除向下取整、0.1+0.2 差 5.55e-17、 2**3**2 是 512 不是 64。

📝 本节和前面几节的关系

上一节把数据装进变量,这一节让它算起来。运算本身不难,难的是那四个「看着对、其实错」的地方。

一、七个运算符,一次看全

Demo 拿 7 和 2 走了七个运算符:+ 得 9、- 得 5、* 得 14、/ 得 3.5、 // 得 3、% 得 1、幂(pow)得 49。 divmod(7, 2) 一次给出 (3, 1),既省一次除法又省一次取余。做分页、算余数的时候用它。 这七个里,只有 / 的结果是浮点。其余六个在整数输入下都是整数。 这一点决定了后面很多东西——包括为什么不能用 / 去算索引。

二、坑一:真除法永远出浮点

7 除以 2 在数学上是 3.5,/ 就老实给 3.5,哪怕两个数都是整数。 Python 3 里 7 / 2 是 3.5,不是 3。要用整商,必须写 //。 这个坑的典型现场是取数组下标:arr[len(arr) / 2] 会抛 TypeError,因为下标不能是浮点。 写成 arr[len(arr) // 2] 才对。

三、坑二:负数整除向下取整

这条最容易错。// 不是「砍掉小数部分」,是「向负无穷方向取整」。 Demo 打了四组:-7 // -2 得 3 余 -1;-7 // 2 得 -4 余 1; 7 // -2 得 -4 余 -1;7 // 2 得 3 余 1。 如果按「向零截断」,-7 // 2 该是 -3,可它给了 -4。 怎么判断哪个对?Demo 加了一列验算:商 × 除数 + 余数。四行结果全部等于原来的被除数。 这是唯一可靠的判据——算术恒等式不撒谎。

四、坑三:浮点不是精确的

0.1 + 0.2 打出来是 0.30000000000000004,0.1 + 0.2 == 0.3 是 False,差 5.55e-17。 这不是 Python 的问题,是二进制浮点的固有性质:0.1 在二进制里是无限循环小数,存进 64 位必然截断。 两条出路:判等写 abs(x - y) < 1e-9;算钱用整数「分」或者 decimal.Decimal。 Demo 用 Decimal 算了一遍,结果正好是 0.3。 算钱这件事上没有中间路线。用浮点算金额,迟早会差一分钱——而对账系统不认「差不多」。

五、坑四:优先级与结合方向

2 + 3 * 4 得 14,因为 * 先算;(2 + 3) * 4 得 20。这个大家都知道。 容易错的是幂:pow(2, pow(3, 2)) 得 512,不是 64。 因为幂运算是右结合,先算 3 的 2 次方得 9,再算 2 的 9 次方。 1 + 2 > 2 得 True,因为算术比比较先算——先得 3,再判 3 > 2。 这条在多条件判断里很关键,写错一处整个条件语义就变了。 Demo 最后量了一次大整数乘法:2000 次 *= 99991,耗时 1.68 毫秒,结果有 33220 个二进制位、 10000 位十进制。 顺手撞到一个真实限制:Python 3.11 起默认不准把超过 4300 位的整数转成十进制字符串, Demo 里被拦下了,显式放宽上限才转成功。这是防暴力破解的护栏,不是 bug。

六、动手 Demo:加减乘除之外的四个坑

先用 7 和 2 走一遍七个运算符并试 divmod;再列四组正负组合,用「商 × 除数 + 余数」验算整除方向; 然后对比 0.1 + 0.2 与 Decimal;最后测四个优先级例子,并量一次 2000 步大整数乘法的耗时。

a, b = 7, 2
print(a + b, a - b, a * b, a / b, a // b, a % b, a ** b)
print(divmod(a, b))                   # (3, 1)

for x, y in ((-7, -2), (-7, 2), (7, -2), (7, 2)):
    print(x // y, x % y, x // y * y + x % y)   # 末列恒等于 x

print(repr(0.1 + 0.2))                # 0.30000000000000004
print(2 ** 3 ** 2)                    # 512,右结合

big = 1
for _ in range(2000):
    big *= 99991

实跑输出:

--------------------------------------------------------------------
运算符:加减乘除之外的四个坑
--------------------------------------------------------------------
  式子     结果
  ------------
  a + b       9
  a - b       5
  a * b      14
  a / b     3.5
  a // b      3
  a % b       1
  a ** b     49
  divmod(a, b)             (3, 1)

  坑一:真除法永远出浮点。7/2 是 3.5,不是 3。
  坑二:负数整除向下取整,不是向零截断。
  式子       商   余   商*除数+余
  ------------------------------
  -7 // -2    3   -1           -7
  -7 // 2    -4    1           -7
  7 // -2    -4   -1            7
  7 // 2      3    1            7
  最后一列处处等于左边被除数——这是取整方向对不对的唯一判据。

  坑三:浮点不是精确的。
  0.1 + 0.2                0.30000000000000004
  0.1 + 0.2 == 0.3         False
  差多少                   5.55e-17
  Decimal 算一遍           Decimal('0.3')
  要判等就写 abs(x - y) < 1e-9;要算钱就用整数分或 Decimal。

  坑四:优先级。** 比 * 高,* 比 + 高,+ 比比较符高。
  式子          结果   为什么
  ---------------------------------------
  2 + 3 * 4     14     先乘再加,14
  (2 + 3) * 4   20     括号先算,20
  2 ** 3 ** 2   512    幂是右结合,先 3**2
  1 + 2 > 2     True   先加后比

  2000 次大整数乘法        1.74 毫秒
  结果有多少二进制位       33220 位
  转成十进制字符串         被拦下:Exceeds the limit (4300 digits) for integer string conversion
  放宽上限再转             10000 位
  Python 3.11 起默认不准把超过 4300 位的整数转成十进制字符串,
  这是防暴力破解的护栏,不是 bug。真要转就显式放宽。

七个数一行看完:9、5、14、3.5、3、1、49。 只有 / 出浮点,其余六个都是整数——这是选运算符时的第一判据。 负数那组的末列全是 -7、-7、7、7,正好是被除数。能用恒等式验算的结论,就别靠背。 浮点那三行:0.30000000000000004、False、5.55e-17。 接着 Decimal 打出 Decimal('0.3'),干净利落。 pow(2, pow(3, 2)) 是 512。想成 64 的人,是把幂当成了左结合。 大整数那两行:1.68 毫秒、33220 个二进制位。 转十进制时被 4300 位的默认上限拦下,显式放宽后得到 10000 位。

30 秒实验

  1. 把 7 和 2 换成 -7 和 2,重新对一遍七个运算符,看哪个变了号。
  2. 用 Decimal 算一遍 0.1 + 0.2,再用浮点算一遍,把两个结果存成字符串对比。
  3. 想一想:为什么幂运算设计成右结合?

常见误区

  1. 「/ 和 // 差不多」——一个出浮点一个出整数,用错就报 TypeError。
  2. 「// 就是砍掉小数」——负数时方向相反,-7 // 2 是 -4。
  3. 「浮点误差可以忽略」——算钱的时候,差一分钱就过不了账。
自测三问(点开对答案) 1. `7 / 2` 和 `7 // 2` 分别得什么? —— `/` 得 3.5(浮点),`//` 得 3(整数)。 2. `-7 // 2` 是多少?怎么验证? —— -4;用 商 × 除数 + 余数 = -4×2+1 = -7 验算。 3. `pow(2, pow(3, 2))` 为什么是 512? —— 幂运算右结合,先算 3 的 2 次方得 9,再算 2 的 9 次方得 512。

算完了,接下来让程序自己判断——条件语句决定走哪条路。

相关笔记