ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python运算符全解析:从算术到位运算,避开优先级陷阱

Python运算符全解析:从算术到位运算,避开优先级陷阱 先说个事。我接触 Python 这么多年见过不少“写了半年 Python 还在用最笨方式处理数字”的开发者。其实很多看似繁琐的活Python 的运算符一套组合拳就能干净利落地解决。但运算符这东西单独拎出来每个都认识混在一起写就容易翻车优先级记错、整除取余方向搞反、逻辑运算返回值理解错位这些都是新手高频踩坑点。这篇内容我就把自己常用的 Python 五类运算符——算术运算符、赋值运算符、比较运算符、逻辑运算符、位运算符——彻底拆开讲一遍。每个运算符我都会配上实际场景告诉你它到底能解决什么问题以及在真实代码里哪里容易出 bug。适合刚学 Python 的入门者也适合写了一段时间但想把基础夯实的同学。看完你至少能做到看到一段混合运算代码能准确判断执行顺序遇到 和 is 不纠结写权限判断、状态位判断时直接上手位运算。1. 算术运算符——不是只有加减乘除那么简单1.1 先认识这七个基本符号包括两个容易踩坑的运算Python 的算术运算符一共七个、-、*、/、//、%、**。前四个不用多说重点聊后面三个。/是普通除法无论两个操作数是整数还是浮点数结果都返回浮点数。这是 Python 3 和 Python 2 最大的行为差异之一。Python 2 里5 / 2结果是2直接丢弃小数部分Python 3 里5 / 2结果是2.5。很多从 Python 2 转过来的老程序员最不适应的就是这一点。如果你确实需要整数结果就用//。//叫整除运算符也叫地板除。它返回不大于真实商的最大整数。重点来了它对于正数很好理解7 // 2 3但对于负数很多人会栽跟头。-7 // 2在 Python 里结果是-4不是-3。原因就是它遵循“向下取整”原则-3.5向下取整是-4。C 语言或者 Java 里-7 / 2是-3因为它们向零取整。很多人把其他语言的思维带进来结果在分页、索引计算上出现莫名奇妙的偏移。%是取余运算符它和//是一对搭档。Python 里有个恒等式a (a // b) * b (a % b)。这个公式意味着取余结果的符号始终跟除数一致。-7 % 2在 Python 里结果是1因为-7 // 2 -4-4 * 2 -8要加1才能回到-7。这个行为在做循环数组索引时特别有用比如(-1) % 5结果是4可以直接安全地拿到环形列表的上一个位置不用额外写边界判断。**是幂运算符2 ** 10就是 1024。它比内置函数pow()更直接写数学公式的时候代码更干净。这些运算符还有一个隐藏能力它们可以作用于字符串和列表。ab cd得到abcdab * 3得到ababab[1, 2] [3]得到[1, 2, 3]。字符串拼接、重复、列表合并都是日常高频操作。1.2 浮点数精度问题0.1 加 0.2 为什么不等于 0.3这个话题老生常谈但我还是要讲因为它在运算符场景里太典型了。你在交互式环境里输入0.1 0.2得到的结果是0.30000000000000004。这不是 Python 的 bug是 IEEE 754 浮点数标准的通病。二进制无法精确表示十进制小数0.1就像十进制无法精确表示1/3一样。这给日常开发带来的直接影响是用直接比较浮点数运算结果极容易翻车。比如你算0.1 0.2 0.3结果是False。实际项目里处理金额、科学计算我建议两条路用decimal.Decimal处理金额类数据Decimal 能做到十进制精确表达实在不想引入新类型就统一用round()或者math.isclose()做比较。from decimal import Decimal result Decimal(0.1) Decimal(0.2) print(result) # 0.3 import math print(math.isclose(0.1 0.2, 0.3, rel_tol1e-9)) # Truemath.isclose是 Python 3.5 之后提供的标准函数比手动abs(a - b) 1e-9更严谨因为它同时考虑了相对误差和绝对误差。1.3 实操建议什么时候用整除和取余我刚学 Python 的时候始终不理解为什么要单独搞一个//。后来发现它简直是编程中的瑞士军刀。分页功能是最典型的场景。假设每页显示 10 条数据总共有 95 条页数就是(95 10 - 1) // 10 10。用整除做向上取整比调math.ceil更直观。时间换算也靠它。把秒数3661转成1小时1分1秒total 3661 hours total // 3600 minutes (total % 3600) // 60 seconds total % 60判断奇偶用x % 2循环队列索引用(index 1) % capacity判断闰年用(year % 4 0 and year % 100 ! 0) or (year % 400 0)。这些都是取余运算的经典应用。提示//和%的优先级跟*、/同级比、-高。所以a b // 2实际是a (b // 2)不是(a b) // 2写代码时最好加括号减少阅读负担。2. 赋值运算符——搞清楚“绑定”而不是“等于”2.1 等号不是数学里的等号是“绑定”Python 里做的事情是把右边的对象绑定到左边的名字上。它不是数学意义上的“等于”。a 1表示创建一个整数对象1然后让变量名a指向它。理解这一点就能解释很多诡异现象。Python 允许链式赋值a b c 10。这条语句会让a、b、c三个名字全部指向同一个整数对象10。整数是不可变对象所以这种共享不会造成问题。但如果对象是可变类型链式赋值就要非常小心。a b [] a.append(1) print(b) # [1]这个坑我在真实代码里见过太多次了。两个列表变量共享了同一个对象改a就是改b。正确做法是拆开赋值a [] b []还有一类高频操作是解包赋值。Python 里可以直接a, b b, a交换变量不需要第三个临时变量。它右边的b, a会先被打包成一个元组(b, a)然后再解包给左边。列表解包和带星号解包也很实用first, *rest [1, 2, 3, 4] print(first) # 1 print(rest) # [2, 3, 4]带星号的解包在很多数据处理场景里非常香比如切割数据集的第一条作为测试样本剩下的全部作为训练样本。2.2 增量赋值看起来是赋值实际上是运算加赋值Python 提供了一组复合赋值运算符、-、*、/、//、%、**等。它们对应的逻辑是先做算术运算再做赋值。但在可变对象上行为有细微差别我单独拿出来讲。对于不可变对象a 1相当于a a 1会创建一个新对象并重新绑定。对于可变对象比如列表a [4]和a a [4]的结果虽然相同但过程不同。在列表上调用的是extend方法是原地修改列表会新建一个列表再赋值给a。a [1, 2] b a a [3] # 原地修改b 也会受影响 print(b) # [1, 2, 3] a [1, 2] b a a a [3] # 新建列表a 指向新对象b 不变 print(b) # [1, 2]这个区别在函数默认参数、共享列表等场景会造成隐蔽 bug。我自己的经验是能不用就尽量用显式的list.extend()至少语义更清晰让别人读代码的时候不至于要回忆一遍 Python 可变对象语义。如果你特别在意性能可以考虑用因为避免了一次无谓的列表拷贝但代价是可读性下降。2.3 一个经典面试题元组里的列表到底能不能改出个题目t ([1], 2)执行t[0] [3]会发生什么我第一次遇到这题也愣了一下。先说结论程序会抛TypeError: tuple object does not support item assignment但t的内容却变成了([1, 3], 2)。这是为什么t[0] [3]的字节码执行过程大致分三步先获取t[0]这个列表对象然后对它执行INPLACE_ADD即调用__iadd__方法这一步把[3]追加到原列表里列表内容变成[1, 3]。最后要把结果存回t[0]但元组是不可变对象所以赋值失败抛异常。异常发生前列表内容已经被修改了。这个例子再次印证元组不保证它内部的可变对象不可变。只要元组里放了列表、字典这类可变对象它们的内容依然可以变。这也是 Python 面试里高频的陷阱题本质考的就是“赋值运算符的修改与绑定分离”这个底层逻辑。3. 比较运算符——值相等、对象相等和链式比较3.1 六种比较符号的底层细节比较运算符包括、!、、、、。这些符号对数字、字符串、列表都适用。字符串和列表的比较规则是字典序。abc abd结果是True因为逐位比较c小于d。列表也一样[1, 2] [1, 3]结果是True先比第一个元素相等再比第二个元素2小于3。这个特性在排序场景特别好用比如按元组排序时会自动按元素顺序决定先后。但有一点要特别小心Python 3 不允许不同类型之间做、比较。1 2会直接抛TypeError。Python 2 里允许整数和字符串比较虽然语义很奇葩。升级到 Python 3 后这种跨类型比较直接被禁掉了。遇到这个异常说明你的数据混入了不同类型先检查数据类型。还有一个隐性行为比较运算的结果都是布尔值True或False。但要注意True和False在 Python 里是int的子类True 1和False 0都成立。所以写if x True不如直接写if x避免和整数产生混淆。3.2 is 和 一个比身份一个比值这是 Python 新手最容易被问晕的一对。比较两个对象的值是否相等is比较两个对象是不是同一个对象也就是它们的id()是否相同。a [1, 2, 3] b [1, 2, 3] print(a b) # True值一样 print(a is b) # False但它们是两个不同的列表对象真正让人困惑的是小整数。在 Python 启动时解释器会预创建-5到256之间的整数对象并缓存所以a 256 b 256 print(a is b) # True缓存命中 c 257 d 257 print(c is d) # False超出缓存范围各自创建对象这个小整数缓存是 CPython 的实现细节规范里没有硬性要求所有 Python 解释器都这样做。所以在业务代码里永远不要用is去比较整数、浮点数、字符串的值一律用。比较对象是不是None则统一用is None这也是一种约定俗成的写法。因为None是单例对象is None更精确也更高效。3.3 链式比较Python 独有的优雅写法Python 允许把多个比较运算符连起来写这是非常优雅的特性。比如判断一个数是否在区间内x 5 print(1 x 10) # True print(1 x and x 10) # 等价写法链式比较不光是语法糖它还会引入惰性求值机制表达式1 x 10会被转换成1 x and x 10并且x只会被求值一次。这个特性在处理函数返回值时很有价值比如start datetime.now() end函数datetime.now()只调用一次保证拿到的始终是同一个时间值避免边界竞争。但这个特性偶尔也会带来意外。链式比较可以写得非常长比如a b c d。可读性会下降而且一旦某个中间结果为False后面的比较就不再执行了。如果你依赖后续表达式产生副作用就会踩坑。我的建议最多链式两到三个比较再多就拆开写。4. 逻辑运算符——短路求值和返回值的真相4.1 and、or、not 基础语义Python 的三个逻辑运算符是and、or、not。普通教材会告诉你它们返回布尔值。但真实行为是and和or返回的是参与运算的对象之一不一定是True或False。这个特性是 Python 比较有个性的地方搞懂它能写出非常简洁的代码。规则就两条a and b先判断a的真假。如果a为假直接返回a不再看b如果a为真返回b。a or b先判断a的真假。如果a为真直接返回a不再看b如果a为假返回b。来几个例子验证print(0 and 10) # 0 print(1 and 10) # 10 print(0 or 10) # 10 print(1 or 10) # 1 print( or default) # default print(hello or default) # hello这个机制背后是短路求值。and只有在左侧为真时才会评估右侧or只有在左侧为假时才会评估右侧。这个特性在代码里最实用的场景有两个一个是防止除零和空值访问另一个是提供默认值。# 防止除零 def safe_divide(a, b): return b ! 0 and a / b # 提供默认值 name user_input or 匿名用户not就简单多了它只返回布尔值。not 0是Truenot hello是False。注意not的优先级比and、or高所以not a and b实际是(not a) and b。4.2 真假值判断不是只有 False 才是假Python 判断真假时除了FalseNone、0、0.0、、[]、()、{}、set()都被视为假值也就是“空”的东西默认就是假。这个设计很有意思判断列表是否为空不需要写if len(items) 0直接写if not items。很多人写代码时会忽略这个特性导致多写不少无意义的代码# 不推荐 if len(items) ! 0: process(items) # 推荐 if items: process(items)不过这个特性也有一个反面坑当你用and或or的返回值做条件判断时要注意它可能不是布尔值。比如[1, 2] and []返回[]虽然[]不是True但在if表达式里会被当作假值处理。所以如果你写if [1, 2] and []:这个分支不会进入。理解这一点才能避开那些“看起来对但运行结果不对”的 bug。4.3 组合优先级not 大于 and 大于 or三个逻辑运算符混用时优先级是not最高然后and最后or。print(True or False and False) # 等价于 True or (False and False) # 结果是 True实际项目里我不建议依赖记忆去判断优先级直接在容易混淆的地方加括号。比如if (a 0 and b 0) or (a 0 and b 0): print(a 和 b 同号)括号不仅消除了歧义也让读者一眼看懂你的意图。代码是写给人看的不是写给解释器看的。另外提一嘴在某些语言里、||是独立于位运算的逻辑符号。Python 没有这类符号直接用字母表示的and、or、not所以也不存在“逻辑与位运算表意混淆”的问题这一点对跨语言开发者反而友好。5. 位运算符——直接操作二进制的高效武器5.1 六种位运算速览位运算符直接对整数的二进制位进行操作。Python 提供了六个按位与、|按位或、^按位异或、~按位取反、左移、右移。它们对“底层原理”的依赖比较直观我逐个配合二进制示例讲# 按位与两个位都是 1 才为 1 print(5 3) # 5101, 3011, 结果 0011 # 按位或有一个位是 1 就为 1 print(5 | 3) # 101 | 011 111 7 # 按位异或两个位不同才为 1 print(5 ^ 3) # 101 ^ 011 110 6 # 左移一位相当于乘以 2 print(5 1) # 101 变成 1010 10 # 右移一位相当于整除 2 print(5 1) # 101 变成 10 2~取反有点反直觉~5结果是-6不是单纯把101变成010。原因在于 Python 的整数是无限位宽的它在补码表示下对每一位取反。这个特性在大多数应用场景里不会直接用到知道即可真正写业务代码时很少直接操作负数取反。5.2 位运算的三个经典应用场景位运算最经典的应用就是权限系统。假设一个用户有三个权限读、写、执行。你可以用三个不同的二进制位表示READ 1 # 0b001 WRITE 2 # 0b010 EXECUTE 4 # 0b100 # 赋予读写权限 perm READ | WRITE print(perm) # 3 # 判断是否有读权限 print(perm READ) # 1非零表示有 # 移除写权限 perm ~WRITE print(perm) # 1整个逻辑非常清晰而且权限的增删查询都是 O(1) 时间。Linux 文件权限chmod 755就是同样的思路读为 4、写为 2、执行为 17 就是三者都有。第二个场景是奇偶判断。判断一个整数是不是偶数最高效的方法是x 1。偶数最低位一定是 0奇数最低位一定是 1。这一招在处理大量数据的过滤场景里能省不少时间numbers range(1000000) even_numbers [x for x in numbers if (x 1) 0]第三个场景是颜色通道提取。很多图形库把颜色存成0xRRGGBB形式要从一个整数里提取红色分量直接移位加掩码color 0x1A2B3C r (color 16) 0xFF g (color 8) 0xFF b color 0xFF print(hex(r), hex(g), hex(b)) # 0x1a 0x2b 0x3c这比调用字符串格式化再解析要快得多尤其在做图像像素级操作时非常关键。5.3 负数的位运算和优先级问题负数参与位运算时Python 的结果可能和其他语言不一样这也是很多人踩过的坑。-5 1在 Python 里结果是-3不是-2。原因是 Python 的右移是向下取整的-5 / 2 -2.5向下取整得到-3。这是和 C 语言向零取整最大的区别。如果你需要向零取整可以不用位运算直接用int(-5 / 2)或者老老实实用//后再修正符号。位运算的优先级排序有个大坑。位运算符的优先级比比较运算符低比逻辑运算符高实际上这里有个微妙的层级和的优先级低于算术运算符高于比较运算符的优先级低于比较运算符^的优先级低于|的优先级低于^。也就是说a b c会被解析成a (b c)而不是(a b) c。a, b, c 5, 3, 1 print(a b c) # 等价于 a (b c) 5 False 5 0 0这类问题极其隐蔽所以我的建议很简单位运算表达式一律加括号。代码多了几对括号但避免了所有优先级歧义。最后还有个有趣的技巧用异或交换两个变量。a ^ b; b ^ a; a ^ b可以做到不借助第三个变量交换值。这个技巧看起来很高端但实际上既不直观也容易出错。现代 Python 里直接a, b b, a就完事了那个异或交换法就当作练习理解一下原理即可。我自己在实际开发里用位运算用得最多的地方还是标志位设计和数据压缩。比如一个整数同时表示多个开关键状态一个 UDP 协议包的状态字段可能只有 8 位每个人为运算错一个 bit 都会导致整个协议解析失败。调试这种问题时候建议先把复杂表达式拆成好几行每步print(bin(...))观察二进制结果很快就能定位问题在哪一位。运算符这些东西从入门到熟练其实没有太多玄学。你只需要把每个运算符的底层行为装进自己的“工具箱”再多写几段不同类型组合的代码就能形成肌肉记忆。我给新手最大的建议是拿到一段复杂表达式先别急着运行拿张纸按优先级拆解一遍再和解释器的输出对比。这样练上十几次优先级和返回值规则自然就刻在脑子里了。
返回列表