引言
这是第二章的第 7 篇。前 6 篇我们分别接触过算术运算符(+ - * / // % **,数字篇)、比较符(==、<,布尔篇与类型转换篇)、逻辑词(and or not,布尔篇)。本篇把散落的运算符收拢成一张完整地图:它们各自是什么、优先级如何排列、结合性怎么作用,并给出一个含算术、比较、逻辑三类运算符的综合表达式从头到尾的逐步推导。第 8 篇将在此基础上深入位运算(& | ^ ~ << >>)。读者应先掌握:五种数字类型的行为(第 2 篇)、真值测试(第 3 篇)、类型转换中的隐式加宽(第 6 篇)——运算符会触发隐式转换,这是理解”为什么 1 + 2.0 是 float”的关键,而优先级则决定这些转换发生在什么顺序上。
概念与原理:优先级与结合性从何而来
运算符(operator)是语言内置的”动词”。Python 的运算符按作用分为几大类。算术运算(arithmetic):+ - * / // % **,其中 // 是地板除、% 取余、** 幂;位运算(bitwise):& | ^ ~ << >>;比较类:== != < <= > >= is is not in not in;逻辑类:and or not;赋值类:= += -= *= /= //= %= **= &= |= ^= <<= >>= 等增强赋值(augmented assignment);另有下一阶段才会细讲的条件表达式(conditional expression)x if 条件 else y 和海象赋值(walrus operator):=。
优先级(precedence)解决”谁先算”:1 + 2 * 3 中乘法先于加法。结合性(associativity)解决”同级谁先算”:8 / 4 / 2 从左到右是 (8/4)/2 = 1,从右到左则会得 16。两条规则都不是拍脑袋定的——它们尽量贴合数学惯例(乘除先于加减)与求值安全(逻辑短路必须从左到右),并把”语法上需要动脑子”的组合留给括号。Python 还有一个翻译惯例:任何你不确定的嵌套,加括号永远合法且最优。括号不仅义务明确,还让读代码的人省一次查表。
各级优先级的完整顺序以 CPython 3.10+ 官方文档(语言参考 “Operator precedence”)为准,从最高到最低:
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1 | (expr...), [...], {...} | 圆括号、列表/字典/集合字面量 |
| 2 | x[i], x[i:j], x(args), x.attr | 下标、切片、调用、属性访问 |
| 3 | await x | 等待表达式(异步,后续课程) |
| 4 | ** | 幂运算(右结合) |
| 5 | +x, -x, ~x | 一元正号、负号、按位取反 |
| 6 | *, @, /, //, % | 乘、矩阵乘(矩阵乘仅限类如 NumPy 矩阵)、除、地板除、取余 |
| 7 | +, - | 加、减 |
| 8 | <<, >> | 左移、右移 |
| 9 | & | 按位与 |
| 10 | ^ | 按位异或 |
| 11 | | | 按位或 |
| 12 | in, not in, is, is not, <, <=, >, >=, !=, == | 比较、成员、身份测试 |
| 13 | not x | 逻辑非 |
| 14 | and | 逻辑与 |
| 15 | or | 逻辑或 |
| 16 | if – else | 条件表达式 |
| 17 | lambda | 匿名函数 |
| 18 | := | 海象赋值 |
同一格内的运算符优先级相同,除 ** 与条件表达式外,都从左到右结合。这张表的三个记忆锚点:幂高于一元负号(-2**2 是 -(2²))、算术高于位运算、位运算高于比较、比较高于逻辑词、not 比 and/or 高。记住锚点,大部分表达式可以不查表直接读。
还有一个值得提前建立的认知:运算符的本质是方法调用。a + b 会被翻译成 a.__add__(b),a == b 翻译成 a.__eq__(b)——每种运算符背后都对应一个特殊方法(special method)。这正是前面几篇里 + 既能加数字、又能拼接字符串的机制:字符串类型重写了 __add__。由此也推出一个重要推论:类型决定运算符的含义,优先级只决定顺序。同一张表里 + 永远是加法符,但在 "3" + "4" 上调用的是字符串的拼接实现,得到 "34"(第 6 篇已见)。后续在面向对象章节,你还会亲手给自定义类实现 __add__ 等魔法方法。
另外,运算符还在版本演化中扩容:Python 3.9 起 | 与 |= 可以对字典做合并操作,d1 | d2 返回新字典、d1 |= d2 原地合并:
d1, d2 = {"a": 1}, {"b": 2}
print(d1 | d2) # {'a': 1, 'b': 2},返回新字典
d1 |= {"a": 9, "c": 3}
print(d1) # {'a': 9, 'c': 3}:同键覆盖,新增补全
这是”运算符对类型开放”的最新佐证——同一运算符在不同类型上有不同约定,阅读文档时要留意每个类型的运算符表。
操作与实现:逐级验证与实战
同级与跨级抽查
用一组真实输出锚定几个最易错的层次关系:
print(1 + 2 * 3) # 7:乘法先于加法(等级 6 > 7)
print((1 + 2) * 3) # 9:括号改变分组
print(-2**2) # -4:幂(等级 4)高于一元负号(等级 5)
print(2**3**2) # 512:幂右结合,等价于 2**(3**2)
print(1 << 2 + 3) # 32:加法先于移位,等价于 1 << (2+3)
print(2 * 3 % 4) # 2:* 与 % 同级,从左到右 (2*3)%4
print(5 & 3 | 4) # 5:& 先于 |,等价于 (5&3)|4
2 * 3 % 4 输出 2 说明同级左结合((2×3)%4 = 6%4 = 2);若从右往左则是 2×(3%4) = 6,判据立现。2**3**2 = 512 则抓住幂的右结合这一唯一例外。
地板除与取余:数学恒等式
/ 恒得 float(第 2 篇提过),// 与 % 才保持整数。它们的语义是”商向负无穷方向取整”,与 C/Java 的”向零截断”不同,Python 保证 被除数 = 商 × 除数 + 余数 恒成立,且余数与除数同号:
print(12 // 5, 12 % 5) # 2 2
print(-12 // 5, -12 % 5) # -3 3:商向负无穷,余数恒为非负(与除数同号)
print(divmod(17, 5)) # (3, 2):同时返回商和余
a, b = -12, 5
print(a == (a // b) * b + a % b) # True:恒等式永远成立
工程中的典型用途是”循环下标到环形缓冲”:index = (index + 1) % buffer_size,Python 保证结果永远落在 [0, buffer_size),负数输入也不会越界——这是 % 相对截断式实现的核心优势。
比较、链式比较与身份测试
比较运算(等级 12)的结果是布尔值。Python 独有的**链式比较(chained comparison)**允许 a < b < c 直接书写,等价于 a < b and b < c,且中间操作数只计算一次:
print(1 < 2 < 3) # True:等价 1<2 and 2<3
print(3 > 2 > 4) # False:等价 3>2 and 2>4
print(2 in [1, 2, 3], 5 not in [1, 2, 3]) # True True:成员测试
a, b = [1, 2], [1, 2]
print(a == b) # True:值相等
print(a is b) # False:不是同一个对象(身份测试)
== 比较值,is 比较身份(是否同一对象,第 1 篇的引用语义直接落地);小整数缓存是 CPython 实现细节,永远用 == 比较数值。另外较新版本(如 CPython 3.12)对 x is 257 这类数字字面量的身份比较会直接输出 SyntaxWarning 提醒你改写成 ==——语言自己都在劝你别这么写。
逻辑短路:返回值不是布尔
and/or 返回的是决定结果的最后一个操作数,不是 True/False。or 遇到第一个真值即停止(短路求值,short-circuit evaluation),and 遇到第一个假值即停止:
print([] or [0] or [1]) # [0]:or 返回第一个真值操作数
print("" and "x") # "":and 遇假即停,返回假值本身
name = ""
print(name or "匿名用户") # 匿名用户:默认值惯用法
not 才保证返回布尔:not "" 是 True。所以 if a or b: 判断真值没问题,但若把 or 的结果当布尔存起来再 == True 比较,就会拿到操作数而非布尔——这是新手的经典困惑。
综合表达式逐步推导
现在把三类运算符放进一个表达式,逐步推导。目标表达式:
result = not 2 + 3 == 5 and 4 < 6 or 1 > 0
先做”括号可视化”:按等级从高到低给每个运算符加上虚构括号,这是读任何复杂表达式的方法论——先圈最高级(算术),再圈次高级(比较),逐级向下,最后剩下的就是骨架。本式的括号化结果为 (not ((2 + 3) == 5)) and (4 < 6) or (1 > 0)。然后逐级归约:
- 第 1 步,算术(等级 6/7):
2 + 3先算得 5,表达式变为not 5 == 5 and 4 < 6 or 1 > 0。 - 第 2 步,比较(等级 12):三个比较同时就绪且互不嵌套,
5 == 5→True,4 < 6→True,1 > 0→True,得not True and True or True。 - 第 3 步,
not(等级 13):not True→False,得False and True or True。 - 第 4 步,
and(等级 14):False and True短路得False(且不计算右侧),得False or True。 - 第 5 步,
or(等级 15):False or True→True。
print(not 2 + 3 == 5 and 4 < 6 or 1 > 0) # True
真实场景一:成绩分档
把链式比较、逻辑与、//、% 揉进一个成绩判定函数,注意 0 <= s <= 100 就是链式比较的教科书用法:
def grade_level(score: int) -> str:
if 0 <= score <= 100 and score >= 60:
return "及格"
if 0 <= score <= 100 and score < 60:
return "不及格"
return "分数无效"
for s in (-1, 0, 59, 60, 100, 101):
print(s, "->", grade_level(s))
这条代码读作”分数在 0 到 100 之间且不低于 60”。若去掉链式写法写 0 <= score and score <= 100,语义相同但多一层嵌套;链式比较让区间判断成为一整个原子逻辑。
真实场景二:闰年判断
闰年规则:“能被 4 整除且(不能被 100 整除或能被 400 整除)“。括号并非多余——and 的优先级高于 or,但逻辑组的意图必须显式化:
def is_leap(year: int) -> bool:
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
print(is_leap(2024)) # True
print(is_leap(2000)) # True:400 整除规则生效
print(is_leap(1900)) # False:能被 100 整除但不能被 400 整除
增强赋值与条件表达式
增强赋值 x += 1 是 x = x + 1 的语法糖。对不可变对象(int、str),它本质是重新绑定新对象(第 1 篇语义),性能收益来自”少写一遍名字”;对可变对象(列表等,后续章节)才可能原地修改。条件表达式 x if cond else y 优先级很低(等级 16),常用于”二选一取值”:
vip = True
price = 10 if vip else 20 # 条件表达式
print(price) # 10
x = 5
x += 1 # 重新绑定为 6
print(x) # 6
选择默认值有两条路:x if x else 默认 与 x or 默认,二者对普通真值相同的变量结果一致,但 x or 默认 在 x 是特殊假值(0、空串)时也会被替换——如果 0 是合法业务值,只能用条件表达式,这是两者唯一的语义分水岭。
易错点与陷阱
1. -2**2 是 -4 而非 4。 幂(等级 4)高于一元负号(等级 5),-2**2 被解析为 -(2**2)。想要正的四次方必须写 (-2)**2。同类还有 ~5(按位取反)与负号的区别,注意一元运算符彼此之间也分等级:-x**2 与 (-x)**2 是两码事——这正是”幂高于一元”的直接后果。
2. or/and 的返回值陷阱。 0 or "ok" 的结果是字符串 "ok",0 and "bad" 的结果是 0,它们都不是布尔。后果一:把结果喂给 if 没问题(走真值测试),但 result or ... == True 这类比较会莫名失败;后果二:忘记短路顺序导致副作用分支没执行——create_user() or notify() 中若 create_user() 返回假值或 None,notify() 才被调用,这可能是设计者没料到的。短路的收益是真性能:items and items[0] 在 items 为空时不触碰下标。
3. 负数地板除与取余的方向。 -7 // 2 是 -4 不是 -3(C/Java 的截断给 -3),-7 % 2 是 1。若按”余数必须非负”实现环形缓冲则完全正确;但若想表达”向零取整”,需要 int(-7 / 2)(第 6 篇的向零截断)或 math.trunc。两种取整语义并存,是跨语言移植时第一处踩坑点。
4. 浮点相等的链式误判。 0.1 + 0.2 == 0.3 为 False(第 6 篇),链式比较对此无能为力——它只解决区间嵌套。真实工程一律 math.isclose(a, b) 做容差比较。还有一类误用是把 3 > 2 > 4 当”不会执行”的蠢代码——它正常求值为 False,编译器不报错,逻辑错误只能靠读题发现。
5. “照搬 C 的括号习惯”会念错表。 优先级表是语言私有的。C 语言里按位与的等级低于相等比较,perm & READ == READ 在 C 中是 perm & (READ == READ);但在 Python 里按位与(等级 9)高于比较(等级 12),同一行代码在 Python 中读作 (perm & READ) == READ——语义恰好相反。print(5 & 3 == 1) 在 Python 输出 True 之后,不要假设其他语言也这样算。
小结
优先级与结合性决定表达式的求值顺序:算术高于位运算,位运算高于比较,比较高于逻辑词;** 右结合且高于一元负号,其余同级左结合;and/or 短路且返回操作数,not 返回布尔。// 与 % 是”向负无穷取整 + 余数恒非负”,并有 a == (a//b)*b + a%b 恒等式锚定。链式比较让区间判断成为原子逻辑。读复杂表达式时,先画括号分层,再逐级归约;写表达式时,宁可多一对括号,也不让读者查表。下一篇将钻进等级 8~11 的位运算——补码、掩码与按位实战。
练习与思考题
- 手推
not 1 > 2 or 3 == 3 and 5 // 2 == 2的求值顺序(按优先级写出每一步),再用 Python 验证结果。 - 写出下列各式的输出并逐条说明理由:
10 // 3、-10 // 3、10 % 3、-10 % 3、10 % -3、7 ** 2 ** 0。 - 实现
clamp(x, low, high):超出区间时返回最近边界,区间内返回原值(提示:用min/max各一次)。 - 思考题:表达式
1 < 2 < 3 < 4求值几次<?链式比较在2 < 1 < 5中是否还会计算第二个比较?运行验证并解释短路规则。