AI 技术博客
返回首页
Python 基础 · 17 分钟阅读

运算符全解:算术、比较与逻辑的优先级

## 引言 这是第二章的第 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` 原地合并: ```python d1, d2 = {"a": 1}, {"b": 2} print(d1 | d2) # {'a': 1, 'b': 2},返回新字典 d1 |= {"a": 9, "c": 3} print(d1) # {'a': 9, 'c': 3}:同键覆盖,新增补全 ``` 这是"运算符对类型开放"的最新佐证——同一运算符在不同类型上有不同约定,阅读文档时要留意每个类型的运算符表。 ## 操作与实现:逐级验证与实战 ### 同级与跨级抽查 用一组真实输出锚定几个最易错的层次关系: ```python 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 保证 **被除数 = 商 × 除数 + 余数** 恒成立,且余数与除数同号: ```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`,且中间操作数只计算一次: ```python 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` 遇到第一个假值即停止: ```python print([] or [0] or [1]) # [0]:or 返回第一个真值操作数 print("" and "x") # "":and 遇假即停,返回假值本身 name = "" print(name or "匿名用户") # 匿名用户:默认值惯用法 ``` `not` 才保证返回布尔:`not ""` 是 True。所以 `if a or b:` 判断真值没问题,但若把 `or` 的结果当布尔存起来再 `== True` 比较,就会拿到操作数而非布尔——这是新手的经典困惑。 ### 综合表达式逐步推导 现在把三类运算符放进一个表达式,逐步推导。目标表达式: ```python 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`。 ```python print(not 2 + 3 == 5 and 4 < 6 or 1 > 0) # True ``` ### 真实场景一:成绩分档 把链式比较、逻辑与、`//`、`%` 揉进一个成绩判定函数,注意 `0 <= s <= 100` 就是链式比较的教科书用法: ```python 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`,但逻辑组的意图必须显式化: ```python 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),常用于"二选一取值": ```python 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 的位运算——补码、掩码与按位实战。 ## 练习与思考题 1. 手推 `not 1 > 2 or 3 == 3 and 5 // 2 == 2` 的求值顺序(按优先级写出每一步),再用 Python 验证结果。 2. 写出下列各式的输出并逐条说明理由:`10 // 3`、`-10 // 3`、`10 % 3`、`-10 % 3`、`10 % -3`、`7 ** 2 ** 0`。 3. 实现 `clamp(x, low, high)`:超出区间时返回最近边界,区间内返回原值(提示:用 `min`/`max` 各一次)。 4. 思考题:表达式 `1 < 2 < 3 < 4` 求值几次 `<`?链式比较在 `2 < 1 < 5` 中是否还会计算第二个比较?运行验证并解释短路规则。