Python表达式的结果并不总按从左到右的直觉执行。原文给出几个典型表达式:2 + 3 * 4 得14;10 - 5 - 2 得3;2 ** 3 ** 2 得512;not True and False or True 得True;3 > 2 == True 得True;10 / 2 * 5 得25.0。它们的差异来自运算符优先级、结合性以及比较链式规则。下面把优先级表、结合性、必须加括号的场景和排查工具串起来。
先看优先级总表,按原文从高到低排列:- Python 运算符优先级(从高到低)
- 1 (expr...) [expr...] {key: value...} f(arg...) x[index] x[index:index] x.attr
- 2 **
- 3 +x, -x, ~x
- 4 *, /, //, %
- 5 +, -
- 6 <<, >>
- 7 &
- 8 ^
- 9 |
- 10 ==, !=, >, <, >=, <=, is, is not, in, not in
- 11 not
- 12 and
- 13 or
- 14 if-else
- 15 lambda
- 16 =, +=, -=, *=, /= 等赋值运算符
- 17 yield
复制代码
用代码验证关键优先级:- print(2 + 3 * 4) # 14
- print((2 + 3) * 4) # 20
- print(-3 ** 2) # -9
- print((-3) ** 2) # 9
- print(2 ** 3 ** 2) # 512
- print((2 ** 3) ** 2) # 64
- print(3 > 2 == True) # True,等价于 (3 > 2) and (2 == True)
- print(10 / 2 * 5) # 25.0
- print(10 / (2 * 5)) # 1.0
- print(not True and False or True) # True
复制代码 这里的几个事实要记牢:-3 ** 2 先算幂再取负;2 ** 3 ** 2 是右结合,先算3**2再算2**9;3 > 2 == True 不是(3 > 2) == True,而是链式比较,等价于(3 > 2) and (2 == True);10 / 2 * 5 中乘除同级,按从左向右计算。
结合性方面,大多数运算符左结合,例如10 - 5 - 2得3,100 / 10 / 2得5.0,10 + 20 - 5得25。右结合的特殊运算符包括幂运算**、一元运算符、赋值运算符和条件表达式。赋值语句a = b = c = 10等价于a = (b = (c = 10))。条件表达式嵌套虽可运行,但可读性差,不适合硬写。
比较运算符还有链式特性:a < b < c等价于(a < b) and (b < c),不是(a < b) < c。1 < 3 < 5为True,1 < 3 > 2也为True。比较运算符优先级高于and,所以1 < 3 and 5 > 2等价于(1 < 3) and (5 > 2)。
短路求值和优先级要分开理解:优先级决定哪些操作数绑定到哪个运算符,短路求值决定哪些部分实际执行。原文用show函数演示:- def show(v):
- print(f'计算了 show({v})')
- return v
- print('True or False and show(1):')
- result = True or False and show(1)
- print(f'结果: {result}')
- print('(True or False) and show(1):')
- result = (True or False) and show(1)
- print(f'结果: {result}')
- print('show(0) or show(1) and show(2)')
- result = show(0) or show(1) and show(2)
- print(f'结果: {result}')
复制代码 第一段中and优先级高于or,但True or ...已经短路,show(1)不会执行;第二段加了括号,show(1)会执行;第三段等价于show(0) or (show(1) and show(2)),show(0)为假后继续计算右侧,show(1)为真时还要计算show(2)来确定and的结果。
记忆时可以分层:访问优先,一元优先,算术优先,移位和位运算,比较和成员,逻辑not/and/or,赋值最后。原文口诀是:括号函数最优先,幂和正负跟后边,乘除整除与取模,加减移位居中间,按位与异或位或,比较成员一条线,非与或来再赋值,lambda最后站一边。口诀用于辅助记忆,遇到复杂表达式仍应加括号。
必须加括号的场景不是风格问题,而是正确性问题。第一类是位运算参与逻辑判断:flags & MASK == MASK等价于flags & (MASK == MASK),因为==优先级高于&,这几乎总是错的;正确写法是(flags & MASK) == MASK。第二类是移位参与算术运算:1 << 2 + 3等价于1 << (2 + 3),结果是32;若想先移位再加3,应写(1 << 2) + 3,结果是7。第三类是not作用于复杂表达式:not a > 0 and b > 0等价于(not (a > 0)) and (b > 0);若想表达两者不都大于0,应写not (a > 0 and b > 0)。第四类是复合赋值与表达式混用,可先算temp再赋给多个变量。- flags = 0b1010
- MASK = 0b1000
- if flags & MASK == MASK:
- print('可能误判')
- if (flags & MASK) == MASK:
- print('匹配')
- result = 1 << 2 + 3 # 32
- result = (1 << 2) + 3 # 7
- if not a > 0 and b > 0:
- pass
- if not (a > 0 and b > 0):
- pass
复制代码 真实案例之一是权限位检查。下面这个函数有bug:user_permissions & required == required等价于user_permissions & (required == required),进一步变成user_permissions & True,也就是user_permissions & 1。修复方式就是给位运算加括号。- def has_permission_bug(user_permissions, required):
- return user_permissions & required == required
- def has_permission_fixed(user_permissions, required):
- return (user_permissions & required) == required
- READ = 0b001
- WRITE = 0b010
- EXECUTE = 0b100
- user_perm = READ | WRITE
- print(has_permission_bug(user_perm, WRITE))
- print(has_permission_fixed(user_perm, WRITE))
- print(has_permission_fixed(user_perm, EXECUTE))
复制代码 另一个常见错误是not的范围。is_active = True、is_admin = False时,not is_admin and is_active等价于(not is_admin) and is_active,碰巧结果可能正确;若要表达不是同时为管理员和活跃用户,必须写成not (is_admin and is_active)。- is_active = True
- is_admin = False
- if not is_admin and is_active:
- print('可以操作')
- if not (is_admin and is_active):
- print('不是同时为管理员和活跃用户')
复制代码 海象运算符:=的优先级很低,几乎总是需要括号。if n := len(data) > 0会被解析成n := (len(data) > 0),n是布尔值而不是长度;正确写法是if (n := len(data)) > 0。result = (x := 10) + 5会得到15,同时x为10。- data = [1, 2, 3]
- if (n := len(data)) > 0:
- print(f'数据长度为{n}')
- result = (x := 10) + 5
- print(result, x)
复制代码 要系统检查优先级,可以写一个测试器,把容易出错的表达式和期望结果列出来:- class PrecedenceTester:
- def __init__(self):
- self.score = 0
- self.total = 0
- def test_question(self, expression, expected, explanation=''):
- self.total += 1
- try:
- actual = eval(expression)
- if actual == expected:
- self.score += 1
- print(f'第{self.total}题 正确: {expression} = {actual}')
- else:
- print(f'第{self.total}题 错误: {expression}')
- print(f' 直觉: {expected} 实际: {actual}')
- if explanation:
- print(f' 解释: {explanation}')
- except Exception as e:
- print(f'第{self.total}题 异常: {e}')
- def run_all_tests(self):
- self.test_question('2 + 3 * 4', 14, '乘法优先级高于加法')
- self.test_question('10 - 5 - 2', 3, '减法从左向右结合')
- self.test_question('2 ** 3 ** 2', 512, '幂运算从右向左结合')
- self.test_question('-3 ** 2', -9, '幂运算优先级高于一元负号')
- self.test_question('10 / 2 * 5', 25.0, '乘除同级,从左向右')
- self.test_question('1 << 2 + 3', 32, '加法优先级高于左移')
- self.test_question('(1 << 2) + 3', 7, '括号改变优先级')
- self.test_question('not True and False', False, 'not > and')
- self.test_question('True or False and False', True, 'and > or')
- self.test_question('3 > 2 == True', True, '链式比较')
- print(f'总计: {self.total}题, 正确: {self.score}题')
- tester = PrecedenceTester()
- tester.run_all_tests()
复制代码 开发工具也能帮忙。现代IDE如PyCharm、VS Code + Pylance会对flags & MASK == MASK这类写法给出警告,提示等值判断和位运算混淆,建议改成(result = (flags & MASK) == MASK)。进一步可以用AST查看Python如何解析表达式:- import ast
- def show_precedence(expr_str):
- print(f'表达式: {expr_str}')
- try:
- tree = ast.parse(expr_str, mode='eval')
- print(ast.dump(tree, indent=2))
- except SyntaxError as e:
- print(f'语法错误: {e}')
- show_precedence('2 + 3 * 4')
- show_precedence('not True and False')
- show_precedence('1 << 2 + 3')
- show_precedence('flags & MASK == MASK')
复制代码 总结一下:优先级表从括号、幂、一元、乘除、加减、移位、位运算、比较、not、and、or到赋值;大多数运算符左结合,幂、一元和赋值等右结合;比较有链式规则;位运算加比较、not加复杂表达式、海象运算符是必须加括号的高风险场景。对求值顺序不确定时,加括号或拆成多步,比事后调试更省成本。 |