
写代码这件事很多人卡在第一个坎上不是因为不懂print、input怎么写而是打开别人的代码看到a // b、x 0xFF、0 age 18这种符号直接发懵。这些符号就是Python里的运算符。刚学完变量和数据类型你很容易觉得基础已经打牢了但真正动手写东西或者读开源项目时你会发现满屏的运算符才是决定你能不能看懂代码的关键。作为“Python入门”系列的第四篇这篇文章专门把运算符从头到尾捋一遍。无论你是刚装好Python还在写第一行print的新手还是学了一阵子却被运算符优先级坑过多次的人这篇都适合你。学完之后至少能看懂大多数入门项目里的符号也能避开那些新手最容易踩的坑。1. 数据是名词运算符是动词先搞清楚它解决什么问题1.1 写代码的本质就是在“操作数据”如果变量和数据类型解决的是“代码里有什么”的问题那运算符解决的就是“这些数据接下来发生什么”。一句话概括变量是名词运算符是动词。price是价格count是数量但price * count才是在算总价age是年龄但0 age 18才是在判断“是否未成年”。没有运算符你写下来的所有赋值语句其实都只是一堆静态的命名标签什么计算、判断、逻辑分支全都做不了。这个理解非常重要因为你后面学的条件判断、循环、函数调用底层都离不开运算符的支撑。哪怕是if user_input quit: break这种看起来和运算符无关的语句里面的和break逻辑组合起来才让程序有了“自主决策”的能力。我见过不少学员学了一周的语法背了一堆print的写法但一到自己写“两个数相加再判断大小”这种实际需求就卡住本质原因就是没把运算符当成一种“行为”来理解老觉得它是语法规则的附属品。1.2 一张表看完Python运算符全景Python的运算符数量在主流编程语言里算中等偏多但分类很清晰。为了方便定位我把常用的运算符先分成八类后面再逐个拆开细讲分类运算符典型作用算术运算符 - * / // % **数学计算、字符串拼接赋值运算符 - * / // % ** :给变量赋值、原地更新比较运算符 ! 比较大小与相等逻辑运算符and or not多条件组合判断位运算符^ ~ 成员运算符in not in判断元素是否存在于容器中身份运算符is is not判断两个变量是否指向同一对象下标运算符[]取序列元素、字典键值学到这里有个很自然的疑问为什么Python不能只保留加减乘除把其他运算符都干掉答案很简单各类运算符服务的场景完全不同。算术运算符解决数值计算比较和逻辑运算符让代码能做分支判断位运算符在底层数据处理里效率极高成员运算符是Python处理容器类型时最省事的高频利器。你在写一个量化交易策略代码、爬虫或者自动化脚本时它们都会轮番登场。理解了这张全景图后面学任何运算符都只是往这个骨架里填肉。2. 算术运算符加减乘除背后的三个隐藏细节2.1 整数除法//和取余%向下取整不是四舍五入大多数人不觉得 - * /有什么好讲的确实它们在数学上和你小学学的一致。但//和%的坑往往在第一次算负数的时候就炸了。先看一段代码print(7 // 2) # 输出 3 print(-7 // 2) # 输出 -4 print(7 % 2) # 输出 1 print(-7 % 2) # 输出 1看到-7 // 2 -4时很多人第一反应是“这不对吧-7除以2应该是-3.5取整应该是-3啊”。这里必须明确Python的整数除法//是向下取整也就是往数轴上更小的那个整数方向取整。-3.5向下取整是-4而不是向零取整的-3。这是Python和C/C/Java一个很重要的差异C家族的整数除法是向零截断。这样设计的结果就是a // b和a % b永远满足恒等式a (a // b) * b (a % b)。不信你算(-7 // 2) * 2 (-7 % 2) (-4) * 2 1 -7完全成立。这种“余数永远和被除数同号”的规则在日期计算、循环索引修正等场景里非常统一。比如你想把索引转到循环列表的合法范围内index % len(lst)永远能拿到正确的结果哪怕是负索引。取余还有一种写法我建议大家掌握divmod(a, b)同时返回商和余数。divmod(17, 5)返回(3, 2)在需要同时拿两个结果时可以少写一行LBYL式的代码。2.2 浮点数运算为什么“不准”0.10.2的真相我讲过很多次一个测试每次现场都有学员露出怀疑人生的表情print(0.1 0.2) # 输出 0.30000000000000004很多人以为这是Python的bug实际上所有的现代编程语言处理浮点数时都存在这个问题。原因很简单计算机内部存储小数用的是二进制而十进制小数比如0.1在二进制里是无限循环小数根本没法精确表示。就像十进制里1/3只能写成0.3333……一样0.1在二进制里也是一个无法精确表示的“循环小数”。既然存储的时候就有误差运算结果自然会有微小的偏差。这个问题的解决思路不是避免浮点数而是分场景处理。如果你做的是普通的数据分析和工程计算这点误差在大多数情况下可以忽略不计但如果你在做金融金额计算比如余额、支付金额绝不能用浮点数直接算而是应该用decimal.Decimal。这里有个小例子from decimal import Decimal print(Decimal(0.1) Decimal(0.2)) # 输出 0.3注意我用的是字符串0.1而不是0.1因为直接传浮点数进去误差已经存在了。另外判断浮点数是否相等时不要写if a b应该用abs(a - b) 1e-9这种“差值的绝对值小于阈值”的方式。2.3 幂运算**和复合计算写一个圆面积计算器**是Python里的幂运算符写2 ** 10返回1024比调用pow(2, 10)更直观而且天然支持浮点数指数比如9 ** 0.5得到3.0。这在实际写业务逻辑时非常常见比如计算复利、面积、物理公式。拿一个经典业务来串一串算术运算符的配合计算圆的面积。import math radius 5.5 area math.pi * radius ** 2 print(f半径为{radius}的圆面积为{area:.4f})这段代码看起来简单但里面有个隐藏的优先级问题**的运算优先级高于*所以radius ** 2会先算然后才跟math.pi相乘。很多新手写math.pi * radius ** 2时会想当然地认为是从左到右算实际上Python按照优先级先处理了幂运算。这也是为什么我建议初学者在学习优先级之前先培养“不确定就加括号”的习惯等熟练之后再简化。圆面积这种例子背后就是一个真实业务的最小模型你给程序几个原始数据它通过运算符组合计算出新的信息这正是程序设计的核心动作。3. 赋值运算符不是数学里的等号3.1 复合赋值运算符一行代码做两件事数学里的等号表达的是左右两边相等而Python里的是“绑定”或“赋值”意思是把右边计算出来的值关联到左边的名字上。这个区别很多人忽略了但一旦搞混后面理解可变对象和函数传参时就会卡住。最常见的进阶用法是复合赋值运算符、-、*、/、//、%、**。比如x 1就是x x 1的简写。它的意义不只是少打字而是明确表达“我要在原值的基础上做更新”这个意图。在写循环计数、累加求和、累乘求阶乘时复合赋值几乎是标配写法。total 0 for score in [89, 72, 93, 85]: total score print(total)这段代码把“累加”这个动作写在了循环体里让代码保持简洁。另一个实用场景是字符串累积message for word in [Hello, World, Python]: message word print(message.strip()) # 输出 Hello World Python需要注意的是对于字符串这种不可变对象来说在循环里实际上每次都在创建一个新的字符串对象效率不高。如果你要做大量的字符串拼接建议改用列表收集再用.join(...)这种方式。这也是我踩过坑之后的心得运算符写作是“正确性优先”但到了性能敏感场景还是要想想背后的对象行为。3.2 链式赋值与海象运算符进阶但很实用链式赋值指的是a b 0这种写法它一次性把多个名字绑定到同一个对象上。当你想初始化一组变量时这种写法很省事。但它有一个很隐蔽的坑如果赋值的是一个可变对象比如a b []那a和b指向的是同一个列表修改其中一个另一个也会变。这种共享引用的行为在业务代码里经常引发奇怪的问题后面第3.3节我会用一个单独的场景细讲。海象运算符:是Python 3.8才引入的它最厉害的地方在于“赋值并返回赋值结果”可以用在表达式内部。最常见的应用是在循环或条件判断里把“需要重复计算的函数返回值”一次性赋值并判断import re pattern re.compile(r\d) while (line : input(请输入一行文本)) ! quit: numbers pattern.findall(line) print(提取到数字, numbers)没有海象运算符之前你得写line input(请输入一行文本) while line ! quit: ... line input(请输入一行文本)对比之下:把赋值和条件判断压缩到一行里减少了一处重复代码也少了一种“漏写末尾更新导致死循环”的可能。我建议初学者可以在小项目里刻意练习使用这个运算符不是为了炫技而是让自己习惯“代码也能返回一个刚算出来的值”这个思维模型。3.3 可变对象引用的大坑为什么b a之后a也变了这是赋值运算符里最值得单独拿出一节来讲的坑。看代码a [1, 2, 3] b a b.append(4) print(a) # 输出 [1, 2, 3, 4]很多初学者会发出灵魂拷问“我只改了b凭什么a也变了”答案是b a做的事情不是把列表复制一份而是把b这个新名字也指向了a指向的那个列表对象。列表是可变对象在内存里只有一份a和b是同一条绳上的两个标签绳另一头只有一个桶。append是在桶里加东西不管用哪个标签去操作桶里的内容都变了。想复制一份互不影响的数据应该用b a.copy() # 浅拷贝 c a[:] # 切片也相当于浅拷贝 import copy d copy.deepcopy(a) # 深拷贝注意浅拷贝和深拷贝的区别。如果列表里只有数字、字符串这种不可变对象copy()足够如果列表里有嵌套的子列表浅拷贝只会复制外层结构内层列表依然共享这时就需要deepcopy。这个知识在写业务代码、清洗数据时特别常见。理解了可变对象与引用你才会真正意识到Python里“赋值”和“复制”是两件不同的事。4. 比较运算符和逻辑运算符让代码学会做判断4.1、is和in的区别三种“相等”别用混初学者最容易混淆的三个运算就是、is和in。它们翻译成中文都带点“等”的意思但语义完全不同。比较的是两个变量的“值”是否相等。比如[1, 2, 3] [1, 2, 3]返回True即使它们是两份各自创建、内存地址完全不同的列表。is比较的是两个变量是否“指向同一个对象”也就是身份标识内存地址是否相同。a [1, 2, 3]; b a; a is b返回True而a [1, 2, 3]也可能返回True但a is [1, 2, 3]极大概率返回False。in则完全不同它不判断相等而是判断“某个元素是否存在”于容器中。3 in [1, 2, 3]返回Trueworld in hello world返回Truename in {name: zhang, age: 30}也返回True因为字典的in默认检查的是键。这里有一个必须养成的习惯判断None时必须用is None绝不要用 None。原因有两点一是None是单例对象用is表达的就是“就是这个对象本身”语义最准确二是个别类可能会重写__eq__让 None返回一些奇怪的结果。协议上也是这么规定的PEP 8 建议用is None。4.2 链式比较和字符串比较语法糖与字典序Python支持链式比较这个特性写起来非常过瘾。比如要判断x是否在0到100之间你可以直接写x 50 if 0 x 100: print(x在0到100之间)这个写法等价于0 x and x 100但可读性高了一个档次。其实Python会把这个链式表达式拆分并且中间的操作数x只会被求值一次所以不需要担心重复计算的问题。这种写法在判断年龄、分数区间等业务逻辑里特别实用。字符串比较也是个容易忽略的细节。Python里字符串可以直接用、、比较规则是按Unicode码点逐字符比较也就是常说的“字典序”。比如print(apple banana) # True因为 a 的码点小于 b print(A a) # True因为 A 的码点是65a 是97这个特性常用于排序、用户名比较等场景。但要小心大小写会影响比较结果所以做用户名校验时最好先统一转成lower()再比较避免用户输入“Admin”和“admin”被当成两个不同字符串。4.3 短路求值与返回真相and/or返回的不一定是布尔值逻辑运算符and、or、not是条件判断的基石。and表示“并且”or表示“或者”not表示“取反”。但Python的逻辑运算符有一个和大多数语言不同的行为and和or不一定返回True或False它们返回的是“最后一次被求值的那个操作数”。规则是这样的a and b如果a为假返回a否则返回b。a or b如果a为真返回a否则返回b。“短路”这个词也来自这里and在判断出左边为假时根本不会再去算右边的值or在判断出左边为真时也一样直接跳过右边。这个特性在写代码时有几个实际用途。最典型的是给默认值username input(请输入用户名) or anonymous如果输入为空字符串假值or就会返回右边的anonymous。另一类是安全地取值data get_data() or [] for row in data: ...如果get_data()返回了None或空列表or []就把数据兜底成一个空列表避免后续循环报错。同时你也要知道Python的“假值”概念False、0、0.0、、[]、()、{}、set()、None都是假值除此之外基本是真值。这个规则直接决定and/or的返回值走向。5. 位运算、成员运算符和身份运算符不常用但关键时刻救命5.1 位运算用整数的二进制换来极致性能位运算这一组包括按位与、|按位或、^按位异或、~按位取反、左移、右移。它们操作的是整数在内存中的二进制比特位。举几个实际应用。判断奇偶可以写成n 1一个数的二进制最低位如果是1那它就是奇数如果是0就是偶数。这比n % 2 1快一点但因为Python本身性能开销较大性能差异基本感觉不到好处是可以让你在C语言类项目里无缝切换思路。更常见的应用是状态标志位比如用整数每一位代表一种权限READ 1 # 二进制 001 WRITE 2 # 二进制 010 EXECUTE 4 # 二进制 100 permissions READ | WRITE # 011同时拥有读和写权限 if permissions READ: print(有读权限) if permissions EXECUTE: print(有执行权限)这种写法在权限系统、网络协议解析、颜色通道处理里非常常见。还有和可以实现快速的乘以2和除以2n 1等价于n * 2n 1等价于n // 2。对于新手我不建议在业务代码里滥用位运算来“炫技”因为可读性差。但当你遇到性能瓶颈、需要处理底层数据时这些符号你至少得能看懂。5.2 成员运算符in一行代码搞定包含关系判断in大概是Python里最让人幸福感爆棚的运算符之一。在其他语言里你要写一个循环去遍历数组检查某个元素是否存在在Python里只需要一行if admin in [admin, root, user]: print(用户名已被占用)in可以作用于列表、元组、字符串、字典、集合。字典里它检查的是键不是值。这一点用的时候要小心因为如果你写if value in some_dict但想检查的是值就会得出错误结果。此时你更需要的可能是遍历所有值或者改用if target in some_dict.values()。in的效率也有讲究。如果你频繁做成员判断优先把容器换成集合set因为list的in是逐个遍历时间复杂度是 O(n)而set底层是哈希表时间复杂度是 O(1)。在处理大量数据时这个差距会非常明显。我写爬虫做去重时就吃过列表in的亏几千条数据还能跑几十万条数据直接慢到怀疑人生换成集合之后速度几乎是瞬时的。5.3is的适用边界别拿is比较数字和字符串身份运算符is在判断None时是标准用法但我见过不少人喜欢用它来比较整数或字符串因为偶尔它会“碰巧”返回正确结果。比如a 256 b 256 print(a is b) # True小整数被缓存了 print(257 is 257) # 在命令行中可能是 False在编译优化中可能又是 TruePython解释器会缓存一些小整数对象通常是 -5 到 256导致a is b在数值较小时返回True。字符串也有类似的“驻留”机制部分只含字母数字的短字符串会被复用。但这种行为属于解释器优化细节不是语言规范你绝不应该依赖它。用is比较数字或字符串是典型的不可靠代码它会让你在调试时完全摸不着头脑。记住一个简洁的规则值是否相等用对象身份是否相同用is是否包含用in三者各司其职。6. Python运算符优先级与一个真实的排错过程6.1 优先级和结合性**是右结合混合运算从右往左运算符优先级是所有运算符知识的“终极整合”因为它决定了一段复杂的混合表达式到底按什么顺序执行。Python官方文档有完整的优先级表格这里我先给一个精简速查表从高到低排列优先级运算符高**x-x~x*///%-^!isinnotand低or除了优先级还有一个容易忽略的概念叫“结合性”。绝大多数运算符是左结合的也就是从左往右计算比如10 - 2 - 3等价于(10 - 2) - 3。但幂运算符**是右结合的2 ** 3 ** 2实际上等于2 ** (3 ** 2)也就是2 ** 9 512而不是(2 ** 3) ** 2 64。我见过有人信誓旦旦地以为结果是64结果拿给老板看数据时被当场打脸。遇到这种不常用的结合性一个括号就能解决别硬记。6.2 一个完整的排查链路为什么评分结果少了10分下面分享一个我之前调参时真实遇到的排查过程场景是实现一个简单的成绩计算函数。需求是一个学生成绩的最终得分是“笔试成绩和加分项先加权求和再整体乘一个难度系数0.6”笔试占40%加分项占10%加分项需要先除以2。我第一次写的代码和预期完全对不上score 80 # 笔试成绩 bonus 20 # 加分项 expected (score * 0.4 bonus // 2) * 0.6 # 我的期望值是 (32 10) * 0.6 25.2 actual score * 0.4 bonus // 2 * 0.6 # 这行是我第一次写的 print(actual) # 输出 38.0第一次看到38.0的时候我的第一反应是“加分项乘错了”。于是我先做减法定位用actual - score * 0.4拿到结果为6.0再用bonus // 2 * 0.6单独验证得到6.0说明这一步内部是先从20 // 2 10再10 * 0.6 6.0没有问题。问题出在整体结构上因为我少写了括号*和//这些同一优先级的运算符都是从左到右结合所以整段代码实际分组成了(score * 0.4) ((bonus // 2) * 0.6)也就是说“加分项先被除以2再乘系数0.6最后才和笔试部分相加”。而我想要的却是“笔试部分和加分项先求和再整体乘0.6”。定位到原因后修复只需要加一对括号actual (score * 0.4 bonus // 2) * 0.6 print(actual) # 输出 25.2这个例子的教训有三个第一混合了*、//、的表达式足够复杂时人类大脑很难精确模拟机器的执行顺序别对自己的心算太自信第二排查问题不要整体怀疑先逐步打印中间结果把“哪一步不符合预期”定位出来第三括号永远是最好的沟通工具它既告诉Python解释器计算顺序也为后来读代码的人明确意图。6.3 我的优先级实操经验能用括号说清楚就别让读者猜在写了不少业务代码、也带过一些新人之后我养成了一个习惯只要是三元以上混合运算我会直接加括号即使不加括号结果也一样。比如if (a 0 and b 0) or c 0:我宁愿多写一对括号也不愿意让读代码的人去翻优先级表。这样做的原因很简单运算符优先级是机器理解的规则但代码最终是给人读的人的短期记忆里装不下太多语法细节。再补充一个常见的运算符优先级误区很多新手会混淆比较运算符和赋值运算符的顺序。比如if x 5:这种写法Python会直接报语法错误因为是赋值而不是比较条件里不能做裸赋值。同理if a b 0:如果要表达的是“a与b按位与后大于0”必须写成if (a b) 0:因为比较运算符的优先级高于按位或按位与。这种细节光看文章记不住最好是在自己的代码里故意写错几次、见识几次报错和错误结果印象就深刻了。我个人在给新项目做代码评审时最关注的就是这种“看起来正确但优先级理解有误”的表达式。它不报错但结果就是不对而且极难定位。所以我的最终建议很朴素新手阶段老老实实加括号不要觉得自己写x a * b c / d就很帅等你能准确说出每一个符号的优先级时再考虑精简表达也不迟。