运算符优先级
Python 的运算符优先级决定了表达式中多个运算符的计算顺序。当不写括号时,Python 按照严格的优先级规则自动分组。理解完整优先级表、结合性规则以及常见陷阱,是编写正确且可读代码的基础。
完整优先级表
以下表格从高到低列出 Python 运算符的优先级。同一行内的运算符优先级相同。
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 最高 | ()、[]、{} | 括号、索引、切片、字典/集合字面量 |
| ↑ | await x | await 表达式 |
| ↑ | ** | 幂运算 |
| ↑ | +x、-x、~x | 一元正号、一元负号、按位取反 |
| ↑ | *、@、/、//、% | 乘、矩阵乘、除、地板除、取模 |
| ↑ | +、- | 二元加、减 |
| ↑ | <<、>> | 位移 |
| ↑ | & | 按位与 |
| ↑ | ^ | 按位异或 |
| ↑ | | | 按位或 |
| ↑ | ==、!=、<、>、<=、>=、is、is not、in、not in | 比较运算 |
| ↑ | not x | 逻辑非 |
| ↑ | and | 逻辑与 |
| 最低 | or | 逻辑或 |
| 最低 | if – else | 条件表达式 |
| 最低 | lambda | lambda 表达式 |
| 最低 | := | 海象运算符 |
注意:海象运算符 := 的优先级实际上低于大多数运算符,因此在混合使用时通常需要括号。
结合性
当多个相同优先级的运算符连续出现时,结合性决定计算顺序。
左结合(从左到右):大多数运算符是左结合的,包括 +、-、*、/、//、%、&、|、^、<<、>>、==、!=、<、> 等。
>>> 10 - 5 - 2
3 # (10 - 5) - 2 = 3,不是 10 - (5 - 2) = 7
>>> 100 / 10 / 2
5.0 # (100 / 10) / 2 = 5.0,不是 100 / (10 / 2) = 20.0
>>> 8 >> 2 >> 1
1 # (8 >> 2) >> 1 = 1,不是 8 >> (2 >> 1) = 4
右结合(从右到左):只有幂运算 **、一元运算符和条件表达式是右结合的。
>>> 2 ** 3 ** 2
512 # 2 ** (3 ** 2) = 2 ** 9 = 512,不是 (2 ** 3) ** 2 = 64
>>> - - 5 # 一元负号右结合
5 # -(-5) = 5
括号使用建议
优先级规则的存在是为了减少括号的使用,但代码的可读性永远优先于省略括号。以下情况建议显式加括号:
- 混合不同类别的运算符时:
# 容易误解
result = a + b << 2 # 是 (a + b) << 2 还是 a + (b << 2)?
# 清晰写法
result = (a + b) << 2
- 比较运算与位运算混合时:
# 危险:& 优先级低于 ==
if flags & MASK == VALUE: # 解析为 flags & (MASK == VALUE)
pass
# 正确写法
if (flags & MASK) == VALUE:
pass
- 逻辑运算与比较混合时:
# 虽然正确但可读性差
if a < b and c < d or e < f:
pass
# 清晰写法
if (a < b and c < d) or e < f:
pass
- 任何你不确定优先级的情况:
"拿不准就加括号"是 Python 社区的最佳实践。多余的括号不会降低性能,但能显著降低维护成本。
常见陷阱
陷阱 1:幂运算与一元负号
这是 Python 中最著名的优先级陷阱。** 的优先级高于一元负号 -,因此 -3**2 被解析为 -(3**2) 而非 (-3)**2。
>>> -3 ** 2
-9 # -(3**2) = -9
>>> (-3) ** 2
9 # 加括号才能得到 9
这与数学中的惯例一致(指数优先于负号),但与部分编程初学者的直觉相反。
陷阱 2:位运算与比较运算
所有位运算符的优先级都高于比较运算符,这导致位掩码检查必须加括号。
>>> 5 & 3 == 1
False # 5 & (3 == 1) → 5 & False → 5 & 0 → 0 → False
>>> (5 & 3) == 1
True # 正确的掩码检查
陷阱 3:逻辑运算与比较运算
not 的优先级低于比较运算,因此 not a == b 被解析为 not (a == b),这通常是期望的行为。但如果想表达 (not a) == b,必须加括号。
>>> not 5 == 5
False # not (5 == 5) → not True
>>> (not 5) == 5
False # False == 5 → False
陷阱 4:链式比较的隐含逻辑
链式比较 a < b < c 等价于 a < b and b < c,但 and 的优先级低于比较运算,因此链式比较天然正确分组。然而,不要试图在链式比较中混合使用其他运算符:
>>> 1 < 2 < 3
True # 1 < 2 and 2 < 3
# 不要这样写
>>> 1 < 2 + 1 < 4
True # 1 < 3 and 3 < 4,虽然正确但可读性下降
陷阱 5:海象运算符的括号
海象运算符 := 的优先级很低,在大多数表达式中需要括号来明确分组。
# 错误:解析为 x := (5 > 3)
if x := 5 > 3:
print(x) # True,不是 5
# 正确
if (x := 5) > 3:
print(x) # 5
陷阱 6:字符串拼接与算术运算
+ 既用于数字相加也用于字符串拼接,但它们的优先级相同。混合使用时会按从左到右的顺序计算。
>>> "result: " + 5 + 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> "result: " + str(5 + 3)
'result: 8'
快速记忆口诀
虽然完整记忆优先级表并不现实,但可以遵循以下原则:
- 括号最高:任何不确定的地方都用括号。
- 幂运算高于乘除:
2**3*4是(2**3)*4 = 32。 - 乘除高于加减:
1+2*3是1+(2*3) = 7。 - 位运算高于比较:比较前先做位运算,但掩码检查要加括号。
- 比较高于逻辑非:
not a == b是not (a == b)。 - 逻辑非高于逻辑与,逻辑与高于逻辑或:
not a and b or c是((not a) and b) or c。
验证优先级的方法
当对某个表达式的分组不确定时,可以用以下方法验证:
# 方法 1:加括号对比结果
>>> -3**2
-9
>>> -(3**2)
-9 # 确认 ** 优先级更高
# 方法 2:使用 ast 模块查看解析树
import ast
tree = ast.parse("-3**2", mode='eval')
print(ast.dump(tree, indent=2))
# 输出显示 UnaryOp(op=USub(), operand=BinOp(left=Constant(value=3), op=Pow(), right=Constant(value=2)))
# 确认先算 3**2,再取负
运算符优先级是编程语言语法的基础规则。Python 的优先级设计总体上符合数学惯例,但位运算与比较运算的交叉、幂运算与一元运算符的关系,以及海象运算符的低优先级,都是实际编码中容易出错的地方。培养"加括号明确意图"的习惯,比背诵完整优先级表更有价值。