资讯动态

Python运算符完全指南:从优先级到重载的实战解析

发布时间:2026/10/19 6:10:00 来源:尧图企业网站定制
1. 别急着背优先级先建立对运算符的直觉如果只让我挑一个“看着简单、用着翻车率最高”的语法点我会选 Python 运算符。它不需要导入、不需要安装几乎每行代码都在用但很多写到一两万行的人遇到not a in b、-3 ** 2、is和这种组合时还是会愣一下。这篇就把运算符从使用直觉到优先级从位运算到重载讲一遍适合刚入门想补基础的人也适合写了几年代码但偶尔被小细节绊倒的开发者。里面的例子都是我在本地解释器里实际跑过的你直接复制就能验证。1.1 运算符的本质动词而不是符号很多人学运算符的第一个误区是把它当成“数学符号的搬运工”。看到就想到加法看到*就想到乘法。这么理解不算错但会限制你对 Python 表达能力的感知。我更愿意把运算符当成“动词”。a b不是在描述两个数字相加而是在对a和b这两个对象执行一个动作。这个动作的具体行为由对象的类型决定。数字加数字是算术加法字符串加字符串是拼接列表加列表是合并列表。同样一个在不同对象身上做了完全不同的三件事这个“多态”的味道才是 Python 运算符真正值得学的地方。这一点在后面讲运算符重载时会体现得更明显。Python 中、-、*、这些符号之所以能被自定义类的实例使用是因为每个运算符背后其实都对应一个魔术方法。对应__add__对应__eq__。你写的a b解释器真正执行的是type(a).__add__(a, b)这个函数调用。表达式语法只是写给人类看的糖衣。带着这个视角再去学运算符你就不再是背十几个符号而是在理解一套“由类型决定行为的对象操作协议”。1.2 Python 运算符的全貌六大家族一张表Python 的运算符虽然多但可以按用途分成几个家族。我平时给新人整理资料时习惯直接画一张这样的总表先建立整体地图再逐项深入。运算符家族主要成员典型用途算术运算符-*///%**数值计算、字符串拼接、序列合并比较运算符!比较值的大小或相等性赋值运算符-*///%** ^ 逻辑运算符andornot组合布尔条件、条件判断位运算符^ ~ 成员与身份运算符innot inisis not判断包含关系、判断对象身份这张表不是让你背下来的而是让你心里有个数以后看到一段代码里的运算符先判断它属于哪个家族再想它在这个场景下到底做了什么。尤其是in、is这两个它们的运算结果不是“计算出一个值”而是“判断出一个布尔结果”这和算术运算符的心理模型完全不同。2. 高频四大类算术、比较、赋值、逻辑的实战细节这四类运算符是日常代码中使用频率最高的但也是最容易在细节上翻车的。我逐个讲一讲那些文档里写了但容易被忽略的点。2.1 算术运算符三个容易翻车的边界第一个边界是/和//的区别。Python 3 里/永远返回浮点数哪怕是4 / 2结果也是2.0。而//是向下取整的整除。注意“向下”这两个字它并不是简单地去掉小数部分。print(7 / 2) # 3.5 print(7 // 2) # 3 print(-7 // 2) # -4-7 // 2的结果是-4因为-3.5向下取整会落到-4。很多从 C 或 Java 转过来的人在这里会懵因为那些语言的整数除法是向零取整。Python 的//配合%有一层数学上的自洽a // b和a % b始终满足a (a // b) * b a % b且余数的符号跟随除数。比如-7 % 2结果是1验证一下(-4) * 2 1 -7。这个性质在需要对称分页、时间对齐、周期计算时特别重要。第二个边界是**和一元负号的优先级。代码里经常有-x ** 2这种写法很多人以为它是(-x) ** 2实际上 Python 更早绑定**所以它等价于-(x ** 2)。print(-3 ** 2) # -9 print((-3) ** 2) # 9第三个边界是浮点数运算。0.1 0.2在二进制浮点数里无法精确表示结果约等于0.30000000000000004。这不是 Python 的 bug而是二进制浮点数的通用限制。需要精确十进制计算的场景应该用decimal.Decimal而不是反复四舍五入。2.2 比较运算符链式比较和浮点相等的坑比较运算符最值得推荐的特性是链式比较。Python 允许a b c这种写法它的语义是a b and b c而且中间的表达式只会被求值一次。这个语法在判断区间时非常自然。score 85 if 60 score 90: print(良好)这个写法可读性比score 60 and score 90好太多而且少了变量重复。但比较运算符的坑在于浮点相等判断。永远不要直接写x 0.3去判断浮点结果除非你能保证这些值都能被二进制精确表示。更稳妥的做法是计算绝对误差或者用math.iscloseimport math print(math.isclose(0.1 0.2, 0.3)) # True另外比较运算符返回的是布尔值True或False但它不会自动做类型转换。123 123的结果是False字符串和数字不相等。很多新手在这里以为 Python 会像 JavaScript 一样做隐式转换想多了。2.3 赋值运算符从到海象运算符赋值运算符里最常被问的是到底是不是“原地修改”。这个问题的答案取决于对象的类型。对于不可变对象比如整数、字符串、元组a b本质上是重新计算并绑定新对象。但对于可变对象比如列表a [x]会调用a.__iadd__在大多数情况下是原地修改列表。a [1, 2] b a a [3] print(b) # [1, 2, 3]b 也跟着变了这个行为如果没意识到很容易在几十行之后排查“为什么另一个变量也变了”。如果你不希望原列表被改动用a a [x]而不是a [x]。Python 3.8 还引入了海象运算符:它能在表达式内部完成赋值并返回赋值结果。之前需要两行代码的场景可以压缩成一行data some text if (n : len(data)) 5: print(f长度是 {n})注意n : len(data)必须加括号。因为:的优先级非常低不加括号可能会破坏表达式结构。海象运算符适合“先计算一次然后既要结果又要判断”的场景但不要为了炫技滥用它会让代码变紧凑的同时也增加阅读负担。2.4 逻辑运算符短路机制和返回值逻辑运算符and、or、not的坑主要在两点短路求值和返回值类型。短路求值的意思是a and b如果a已经是假值就不会再去求ba or b如果a已经是真值就不会再去求b。这个特性很有用可以安全写出user and user.name这种表达式因为user为假时后面的属性访问不会执行。反过来说如果你依赖后面的函数调用产生副作用短路可能让副作用消失这需要有意设计。返回值类型是另一个容易忽略的点。and和or返回的不一定是布尔值而是决定结果的其中一个操作数。print(0 and 100) # 0 print(1 and 100) # 100 print(0 or 默认) # 默认很多人以为a and b只会返回True或False于是拿到结果后又包一层bool()。其实bool(0 or x)的结果当然也是布尔值但如果你只是想用返回值作为条件判断Python 会自动使用真值测试不需要额外转换。这个特性也常用于给默认值name input_name or 匿名当输入为空字符串时自动落到默认值。3. 位运算、成员、身份进阶运算符的实际场景前面四类大家天天用下面这三类可能用得少一点但在很多场景里是不可替代的。3.1 位运算在权限、标志和底层解析里当开关位运算操作的是整数的二进制位。是按位与|是按位或^是按位异或~是按位取反和是左移右移。很多刚学的人觉得位运算只在“底层开发”里才有用其实在应用层也很常见。权限系统是一个典型场景。假设一个文件的权限有三个开关READ 1 # 0b001 WRITE 2 # 0b010 EXECUTE 4 # 0b100 permission READ | WRITE # 同时有读和写权限 print(permission) # 3 if permission READ: print(可以读) permission ~WRITE # 去掉写权限 print(permission) # 1这种写法比存多个布尔字段更紧凑而且查权限时只需要一次按位与。需要注意的是Python 的整数是任意精度的~x的结果在概念上是负数比如~2 -3。如果只想要低 8 位的结果通常要配合掩码 0xFF使用。位运算在图像处理、协议解析里也常见。比如从一个像素值里提取红色通道经典写法是red (pixel 16) 0xFF。这种代码可读性不直观所以写的时候一定要加注释说明掩码和位移的含义。3.2 成员运算符字典和子串容易忽略的点in用于判断一个元素是否包含在某个容器里。它的行为取决于容器类型最容易被忽略的是字典。对于字典in检查的是键而不是值d {name: Tom, age: 18} print(name in d) # True print(Tom in d) # False想检查值是否存在得用Tom in d.values()。这看起来是细节但确实有人把字典当成列表用直接在in后面写字典变量名结果半天查不到原因。对字符串in检查的是子串print(world in hello world) # True这里还藏着一个新手困惑空字符串是任何字符串的子串所以 in abc的结果是True。这符合数学定义但第一次遇到时会觉得奇怪。3.3 身份运算符is和的分工比较的是值is比较的是对象身份也就是“是不是同一个对象”。两者用途完全不同混用是常见的隐蔽 bug。None的判断必须用is None这是 Python 社区强烈推荐的写法因为它更快、更明确而且None在 Python 中是一个单例对象。类似地判断布尔值一般不依赖is True但is None是硬规范。小整数缓存是另一个容易产生困惑的点。CPython 会缓存-5到256之间的整数所以a 200 b 200 print(a is b) # True因为 200 在缓存范围内但超出这个范围a 1000 b 1000 print(a is b) # 可能是 False取决于实现这个结果并不是语言规范保证的所以你永远不应该用is来比较整数是否相等。看到a is b时第一反应应该是问自己我真的要比较对象身份吗如果只是想判断两个值一样用永远没错。4. 实操过程运算符重载让自定义类支持加减乘除前面说了运算符本质是对象的行为。接下来我用一个二维向量类完整演示怎么让自定义类支持、-、*这些运算符。这段代码可以直接跑也能作为你以后写自定义数据结构的模板。4.1 魔术方法运算符真正调用的是它们每个运算符背后都有一个魔术方法。下面这些是常用的对应关系运算符魔术方法__add__-__sub__*__mul__/__truediv__//__floordiv__%__mod__**__pow____eq____iadd__没有则回退到__add__当你定义这些方法时表达式v1 v2实际上会调用v1.__add__(v2)。如果v1的__add__返回一个特殊值NotImplemented解释器会尝试v2.__radd__(v1)这是反向运算符方法。4.2 一个可运算的二维向量类我们实现一个最简但规范完整的二维向量class Vector: def __init__(self, x, y): self.x x self.y y def __repr__(self): return fVector({self.x}, {self.y}) def __add__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x other.x, self.y other.y) def __sub__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x - other.x, self.y - other.y) def __mul__(self, scalar): if not isinstance(scalar, (int, float)): return NotImplemented return Vector(self.x * scalar, self.y * scalar) def __rmul__(self, scalar): return self.__mul__(scalar) def __iadd__(self, other): if not isinstance(other, Vector): return NotImplemented self.x other.x self.y other.y return self def __eq__(self, other): if not isinstance(other, Vector): return NotImplemented return self.x other.x and self.y other.y测试一下v1 Vector(1, 2) v2 Vector(3, 4) print(v1 v2) # Vector(4, 6) print(v2 - v1) # Vector(2, 2) print(v1 * 3) # Vector(3, 6) print(3 * v1) # Vector(3, 6)通过 __rmul__ 实现 print(v1 v2) # False这里面有几个细节值得说明。__mul__检查了scalar的类型如果不是int或float就返回NotImplemented而不是主动抛TypeError。这样做的原因是把类型不匹配的处理权交还给解释器让解释器有机会去尝试反向方法。如果你直接抛异常反向方法连执行机会都没有。__rmul__用来支持3 * v1这种写法。当 Python 发现int不知道怎么和Vector相乘时就会回头调用Vector.__rmul__。__iadd__实现了的原地修改。注意它返回self这样v1 v2才会把新的结果重新绑定给v1。如果__iadd__返回别的对象变量就可能被指向新对象。4.3 重载时的三个规范第一保持一致性。如果你重载了最好同时考虑!的行为。Python 3 里定义了__eq__如果没有定义__ne__!会自动取反结果所以通常不用手动写但这只适用于__ne__缺失的情况。更麻烦的是定义__eq__会让类的哈希行为改变。因为这类Vector是可变的被加入字典或集合后如果内容变了哈希值会失效所以 Python 会默认把__hash__置为None导致实例不可哈希。如果你需要可哈希的向量应该设计成不可变类并同时实现__hash__。第二不要直接返回True或False而不做类型判断。Vector(1, 2) 1应该返回False还是NotImplemented返回NotImplemented更合适因为1不是一个Vector比较“向量和整数相等”没有意义。即便返回False看起来也没错但会让后续的反向比较失去机会也容易掩盖逻辑错误。第三运算符方法不要搞复杂的副作用。__add__应该返回一个新对象__iadd__才适合修改自身。如果你让也去修改self调用方会得到出乎意料的结果。5. 优先级与结合性把最常用的规则刻进习惯里优先级问题不是靠背表解决的而是靠理解几个容易错的边界。下面这张表按从高到低列出了主要优先级建议收藏后配合代码测试。级别运算符说明高括号、下标、属性、调用(ab)d[k]obj.attrf(x)**幂运算x-x~x一元正负号、按位取反*///%乘除取余-加减移位按位与^按位异或!inis比较、成员、身份not逻辑非and逻辑与or逻辑或x if c else y条件表达式低lambda:匿名函数、海象赋值这张表里最有用的一条信息是not的优先级低于比较运算符但高于and、or。所以not a in b等价于not (a in b)而not a b等价于not (a b)。这种写法虽然合法但可读性一般。我更推荐用a not in b和a ! b因为它们更直观而且完全避免优先级问题。5.1 三个优先级翻车现场第一个翻车现场是-3 ** 2。前面讲过它在 Python 中是-(3 ** 2)结果是-9。写代码时如果本意是“负三的平方”必须写成(-3) ** 2。这种 bug 很难一眼看出来因为-3 ** 2在数学上通常被认为等于 9。第二个翻车现场是and和or混用。因为and优先级高于or所以a or b and c等价于a or (b and c)。如果你本意是(a or b) and c需要显式加括号。很多逻辑 bug 不是逻辑设计错了而是优先级把条件组合成了另一种形式。判断办法很简单代码评审时只要看到and、or混在同一行就强制自己加括号。括号不会减慢运行速度但会显著减少误解。第三个翻车现场是海象运算符:的优先级极低。它只高于lambda低于条件表达式。所以if n : len(s) 5:这行会被解析成n : (len(s) 5)赋值给n的是布尔值而不是长度。必须写成if (n : len(s)) 5:这是我在代码评审中见过很多次的错误。海象运算符好用但要时刻记得它不是一个“普通运算符”它的绑定能力非常弱。5.2 结合性幂运算从右往左其他大多数从左往右优先级相同的情况下还需要注意结合性。大多数运算符是从左往右结合比如减法10 - 3 - 2等价于(10 - 3) - 2。但幂运算**是从右往左结合所以2 ** 3 ** 2等价于2 ** (3 ** 2)结果是2 ** 9也就是 512而不是(2 ** 3) ** 2的 64。幂运算的右结合性在数学上很常见但如果你不熟悉第一次看到时会觉得奇怪。赋值运算符也是从右往左结合。a b 5会先把 5 赋值给b再把结果传给a。连续赋值在 Python 中是合法的但在我从业经历里它经常让初学者混淆“共享引用”和“值复制”所以建议少用一行只赋一个变量更清晰。6. 常见问题与排查技巧实录运算符相关的报错和怪现象特别多我把这些年看到的高频问题汇总成一张速查表。现象原因处理方式if x 5:报 SyntaxError把赋值当成了比较改成0.1 0.2 ! 0.3浮点数二进制表示误差用math.isclose或DecimalTypeError: can only concatenate list (not int) to list列表只支持列表使用[x]或.append()is判断两个大整数时返回 False大整数没有缓存值比较用True True结果是 2布尔是整数子类不要把布尔当数用(a in abc) True看着别扭返回值本身就是布尔直接当成条件使用x y后另一个变量也变了可变对象的原地修改用x x y显式新建对象6.1 典型报错和排查思路排查运算符问题我有一套固定流程先把表达式拆分再看两侧类型最后验证优先级。举个例子假设你写了result 1 2 3你预期先把1左移 2 位得到 4再加 3 得到 7。但实际结果是 32因为的优先级高于表达式等价于1 (2 3)。让我演示一下拆分排查的方式# 第一步把复杂表达式拆成变量 a 1 b 2 3 result a b print(result) # 32拆开之后错误立刻暴露。这种“拆成临时变量”的方法对任何优先级问题都有效它的本质是把“解释器怎么解析”这个问题变成“我希望它怎么解析”这个更清楚的问题。NotImplemented相关的报错也值得提一下。如果你定义了一个类重载了__add__但v1 v2仍然报TypeError: unsupported operand type(s) for 大概率是你的__add__返回了NotImplemented而另一个对象的反向方法也不存在。检查的时候单独调用一下v1.__add__(v2)看返回什么能快速定位问题。6.2 写代码时能少踩的坑结合我自己的习惯有几条特别实用的纪律可以分享。第一布尔逻辑要用独立性强的变量名不要写一长串嵌套条件。比如if a and b or c and d这种除非你有绝对把握否则先提取成命名变量再在if里组合。这不是性能问题是心智负担问题。第二比较值时常量写左边。比如if value 1如果敲成if value 1会报错但如果你写if 1 value而敲错成if 1 value解释器同样会报错。Python 不会像 C 语言那样有隐藏赋值风险所以这条主要看个人习惯。我更推荐的是写完条件后用眼睛扫一遍确认每个都写全了。第三自定义类的运算符重载要在测试中覆盖“错误类型”的场景。很多人只测Vector Vector不测Vector 1。一旦你的代码在某个回调里接到错误类型直接返回NotImplemented是一种“安静”的失败可能不会报错而是产生奇怪的异常信息。加上边界测试能省掉很多排查时间。第四in判断字典时如果业务上需要判断键存在且值不为空不要只写key in d就完事。这种写法只判断键不判断值。根据业务需要写成key in d and d[key] is not None会安全很多。6.3 一个排查小技巧用repr和type验证两侧对象遇到运算符结果和直觉不一致第一件事不是查优先级表而是打印两侧对象的真实类型和内容。用print(repr(a), type(a))而不是print(a)因为repr能区分字符串123和整数123也能显示None而不是空输出。我见过一个真实案例一段数据处理代码里a b偶尔得到拼接字符串偶尔得到数值相加排查到最后发现上游一个接口在某些条件下返回字符串另一些条件下返回整数。type一打出来问题就清楚了。运算符本身没有错是类型不统一造成的。这种排查思路对所有人都有用。Python 运算符的优先级和绑定规则是固定的出问题的地方几乎总是“两侧对象的类型”或“表达式被解析成了另一种组合”。先排除这两项再往深处找基本不会走弯路。我自己写代码时还有一条习惯凡是运算符重载比较复杂的地方都会单独写一个小文件做冒烟测试。比如上面的Vector我大概率会在文件底部放几行assert确保乘法的左右两种写法都正常不会返回None。这些断言平时不引人注意但改到相关代码时会自动提醒我有没有破坏行为。运算符是程序设计中最基础的连接件连接件一旦不稳定上层代码再漂亮也容易散架。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑