☰
Python运算符完全指南:从优先级到重载的实战解析
2026/10/19 6:09:56 网站建设 项目流程

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.isclose:

import 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已经是假值,就不会再去求b;a 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 f"Vector({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. 优先级与结合性:把最常用的规则刻进习惯里

优先级问题不是靠背表解决的,而是靠理解几个容易错的边界。下面这张表按从高到低列出了主要优先级,建议收藏后配合代码测试。

级别运算符说明
高括号、下标、属性、调用(a+b)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或Decimal
TypeError: 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。这些断言平时不引人注意,但改到相关代码时会自动提醒我有没有破坏行为。运算符是程序设计中最基础的连接件,连接件一旦不稳定,上层代码再漂亮也容易散架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询