012、运算符优先级与结合律:那些让你查半天 bug 的表达顺序

📅 2026/6/22 19:49:45
012、运算符优先级与结合律:那些让你查半天 bug 的表达顺序
012、运算符优先级与结合律那些让你查半天 bug 的表达顺序上周帮同事排查一个线上数据统计的 bug场景很简单计算用户活跃度得分公式是score a b * c d and e or f。同事信誓旦旦说逻辑没问题结果跑出来的数据全是 0 和 1 的极端值完全不符合预期。我盯着这行代码看了三秒直接说“你少写括号了。”他不信我让他打印中间结果果然a b * c先算乘法然后和d比较再和and/or搅在一起——整个表达式的求值顺序完全不是他脑子里想的那样。这种坑我敢说每个 Python 开发者都踩过区别只是你花了多久才意识到问题出在运算符优先级上。优先级表不是让你背的是让你查的Python 的运算符优先级从高到低大致是幂运算 一元运算符 算术运算符 移位运算符 位运算符 比较运算符 赋值运算符 身份/成员运算符 逻辑运算符。但说实话正常人谁记得住和谁优先级高我写了十年 Python每次遇到位运算和逻辑运算混用第一反应还是去翻文档或者直接加括号。真正需要刻在脑子里的是那几个最容易出问题的优先级陷阱算术运算符* / // %高于 -这个小学都学过但很多人写2 3 * 4以为结果是 20结果出来 14 才恍然大悟。别笑我见过生产代码里有人写total price tax * quantity结果税被乘了数量价格反而只加了一次。比较运算符所有比较运算符 ! 优先级相同且低于算术运算符。这意味着a b c d等价于(a b) (c d)这没问题。但坑在于链式比较a b c在 Python 里是合法的等价于a b and b c。如果你写a b c它会被解释为a b and b c而不是(a b) c。这里踩过坑的人举个手。逻辑运算符notandor。这是 Python 里最经典的优先级陷阱。not a and b等价于(not a) and b而不是not (a and b)。a or b and c等价于a or (b and c)因为and优先级高于or。很多人写条件判断时脑子里想的是“如果 a 为真或者 b 和 c 都为真”结果写出来if a or b and c实际执行的是“如果 a 为真或者 b 和 c 都为真”。大多数情况下这恰好是你想要的但一旦逻辑复杂起来这就是 bug 的温床。结合律从左到右还是从右到左大多数运算符是左结合的即从左到右计算。但有两个例外赋值运算符是右结合的a b c 0等价于a (b (c 0))从右往左赋值。这个好理解因为你要先算出右边的值才能赋给左边。幂运算符**也是右结合的2 ** 3 ** 2等价于2 ** (3 ** 2)结果是 512 而不是 64。这里踩过坑的人应该不少因为数学上幂运算通常是从左到右的但 Python 选择了从右到左和数学约定一致因为a^{b^c} a^{(b^c)}。比较运算符的链式比较其实也是一种特殊的结合方式a b c不是简单的从左到右或从右到左而是展开成a b and b c中间变量b只计算一次。这个特性有时候很香比如写if 0 x 10比if x 0 and x 10优雅得多。但别滥用a b c这种写法虽然合法但可读性极差别这样写。那些年我查过的“表达式 bug”案例一条件赋值陷阱# 别这样写resultaifconditionelsebc你以为else后面是b c实际上if-else条件表达式的优先级比低不对条件表达式的优先级其实比低所以a if condition else b c等价于a if condition else (b c)这恰好是你想要的。但如果你写a b if condition else c它等价于(a b) if condition else c也没问题。真正坑的是嵌套条件表达式# 这里踩过坑resultaifcond1elsebifcond2elsec这等价于a if cond1 else (b if cond2 else c)因为条件表达式是右结合的。如果你想要的是(a if cond1 else b) if cond2 else c必须加括号。案例二位运算与比较运算混用# 别这样写ifflags0x010x01:你以为这是“如果 flags 的第一位为 1”实际上优先级高于所以它等价于flags (0x01 0x01)即flags True而True在 Python 里等于 1所以实际上是在检查 flags 的最低位是否为 1。大多数情况下结果碰巧是对的但一旦 flags 的值是 2二进制 102 1结果是 0而2 True也是 0所以没暴露问题。直到有一天 flags 是 3二进制 113 1是 13 True也是 1依然没暴露。真正出问题的是 flags 是 0 的时候0 1是 00 True也是 0。这个 bug 能潜伏很久直到你遇到 flags 是 4 的情况——4 1是 0但4 True是 0还是没暴露。实际上这个 bug 几乎不可能通过测试发现因为True等于 1而 1和 True在大多数情况下行为一致。但如果你写if flags 0x02 0x020x02 0x02是True等价于flags 1完全错了。正确写法是if (flags 0x01) 0x01。案例三逻辑运算与算术运算混用# 别这样写resultabandcord这个表达式等价于((a b) and c) or d因为优先级高于andand优先级高于or。如果你想要的是a (b and c) or d必须加括号。但说实话这种写法不管加不加括号可读性都很差不如拆成多行。我的个人经验不要相信自己的记忆力。我写了十年 Python每次遇到复杂的表达式第一件事是加括号。括号不花钱但查 bug 的时间很贵。如果你觉得加括号显得自己水平低那说明你还没被坑够。一行代码不要超过一个运算符。这不是说不能写a b * c而是说不要写a b * c d and e or f这种“一行流”。拆成多行每行只做一件事加上注释三个月后的你会感谢现在的你。善用dis模块。当你实在搞不清楚某个表达式的求值顺序时用dis.dis(你的表达式)看看字节码Python 解释器会告诉你它到底先算哪个。比如dis.dis(a b * c)会显示先加载b和c做乘法再加载a做加法一目了然。写单元测试时专门测试边界表达式。比如0 and something、1 or something、not 0这些短路求值的情况确保你的逻辑在边界条件下依然正确。代码审查时看到没有括号的复杂表达式直接打回。这不是吹毛求疵这是团队规范。我所在的团队有一条不成文的规定任何包含两个以上不同优先级运算符的表达式必须加括号。这条规定救了我们无数次。最后说一句运算符优先级不是用来炫技的是用来避免 bug 的。如果你不确定加括号。如果你觉得加括号丑那说明你的表达式本身就该重构了。