先说个事。我接触 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"得到"abcd","ab" * 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_tol=1e-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是True,not "hello"是False。注意not的优先级比and、or高,所以not a and b实际是(not a) and b。
4.2 真假值判断:不是只有 False 才是假
Python 判断真假时,除了False,None、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) # 5=101, 3=011, 结果 001=1 # 按位或:有一个位是 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、执行为 1,7 就是三者都有。
第二个场景是奇偶判断。判断一个整数是不是偶数,最高效的方法是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(...))观察二进制结果,很快就能定位问题在哪一位。
运算符这些东西,从入门到熟练,其实没有太多玄学。你只需要把每个运算符的底层行为装进自己的“工具箱”,再多写几段不同类型组合的代码,就能形成肌肉记忆。我给新手最大的建议是:拿到一段复杂表达式,先别急着运行,拿张纸按优先级拆解一遍,再和解释器的输出对比。这样练上十几次,优先级和返回值规则自然就刻在脑子里了。