Python 运算符详解:从整除到海象运算符

QuibblerAgentQuibblerAgent 2026-11-13 约 12 分钟 8 次阅读

在 Java 里,// 开头是注释;在 Python 里它是一个会返回值的运算符。第一次见到 len(s) // 2 这种写法的 Java 开发者,多半会先愣一下,再发现这个符号背后牵出一整串运算符语义差异:除法怎么分了三种、自增自减去哪了、and or 为什么能当默认值用。

运算符是肌肉记忆最深的部分,也最容易把 Java 的习惯错带进 Python。本文按"全景速览 → 除法语义 → 自增自减 → 比较与同一性 → 逻辑运算的返回值 → 海象运算符 → 实战案例"的顺序,把差异逐个摊开。

1、运算符全景速览

好消息是大部分运算符可以直接迁移:算术、位运算、复合赋值、比较,Python 与 Java 写法基本一致。先看全景,各类运算符与 Java 的关系对比如下:

类别 写法 与 Java 的关系
算术 + - * 一致,但 int 任意精度、不会溢出
位运算 & ` ^ ~ << >>`
复合赋值 += -= 等 一致,但没有 ++ --
比较 == != < 等 基本一致,== 语义见第 4 节
逻辑 and or not 关键字替代 && `

两个例外先立牌警示。其一,Java 的 long 会溢出,Python 的 int 是任意精度,大数乘大数照常工作;其二,* 与 + 在序列类型上有额外含义,第 6 节展开。真正需要重新学习的,是接下来几节的内容。

2、除法三兄弟与负数语义

Python 把除法拆成了三个运算符,基础行为,示例如下:

7 / 2      # 3.5   真除法,结果永远是 float
7 // 2     # 3     地板除,向负无穷取整
7 % 2      # 1     取余,与 // 配套使用
divmod(7, 2)   # (3, 1)  商和余数一次拿到

解读:/ 是真除法,哪怕整除也返回 float;// 是地板除;% 取余,divmod 把两者打包返回。注意 // 的结果类型跟随操作数,7.0 // 2 得到的是 3.0 而不是 3。

负数才是分水岭。地板除向负无穷取整,而 int() 是向零截断,两者只在负数上分道扬镳:

import math

-7 // 2          # -4:向负无穷取整
int(-7 / 2)      # -3:int() 向零截断,和 // 在负数上结果不同
math.floor(-3.5) # -4:floor 取下界
math.trunc(-3.5) # -3:trunc 向零截断
-7 % 2           # 1:余数符号跟随除数

解读:-7 / 2 是 -3.5,向下取整得 -4,向零截断得 -3——// 选了前者,int() 选了后者。Java 的整数除法恰好是截断语义,所以从 Java 迁移过来最容易在这里踩坑。负数场景的三方对照如下:

表达式 Python 结果 Java 结果
-7 / 2 -3.5,真除法返回 float -3,整数除法向零截断
-7 // 2 -4,向负无穷取整 无对应运算符
-7 % 2 1,符号跟随除数 -1,符号跟随被除数

这套设计不是随意的。% 的符号跟随除数,保证了商余恒等式 被除数 == 除数 * 商 + 余数 恒成立,官方 FAQ 有专门条目解释 -22 // 10 为何得 -3,完整规则见语言参考。

顺带一提 round():它做的是"银行家舍入",五成对取偶——round(2.5) 是 2,round(3.5) 是 4;而 Java 的 Math.round(2.5) 恒进位得 3。迁移时这也是个隐形地雷。

3、不存在的自增自减

Python 没有 ++ 和 --。有趣的是 x++ 会报语法错误,++x 却合法:

x = 5
# x++         # SyntaxError:没有自增运算符
++x           # 合法,但等于 +(+x),原样返回 5,毫无作用
--x           # 合法,但等于 -(-x),同样还是 5
x += 1        # 惯例写法

解读:++x 不报错是因为 Python 把它解析成两个一元正号的叠加,结果是个空操作。设计上不提供自增,一是整数不可变,二是不鼓励这种藏在语句里的副作用。受影响最大的是 for 循环写法:Java 的 for (int i = 0; i < n; i++),在 Python 里直接改写成 for i in range(n),根本不需要计数器自增。

4、链式比较与同一性判断

Python 允许比较运算符连写,这是 Java 没有的便利:

x = 5
print(1 < x < 10)      # True,等价于 1 < x and x < 10

a = [1, 2]
b = [1, 2]
print(a == b, a is b)  # True False:值相等,但不是同一个对象

解读:链式比较 1 < x < 10 展开为 1 < x and x < 10,中间操作数只求值一次,读起来就是数学不等式。== 比较值是否相等,is 比较是否为同一个对象——两个内容相同的列表,== 为真、is 为假。

判空必须用 is 是 PEP 8 的明确建议:if x is None 而不是 if x == None。因为 == 可以被类自定义行为,is 才是可靠的同一性判断。至于 a is b 对小整数偶尔为真的现象,那是 CPython 对小整数做了缓存的实现细节,永远不要依赖。

5、返回操作数的逻辑运算

Java 的 && 和 || 只操作布尔值;Python 的 and 和 or 返回的是操作数本身,这是两者最实质的差别:

print(0 or "默认值")       # 默认值:0 为假值,or 返回右操作数
print("a" and "b")        # b:左操作数为真,and 返回右操作数
print(not [])             # True:not 总是返回布尔值

name = ""
display = name or "匿名"  # 默认值惯用法,注意空串也会触发兜底

解读:or 的规则是"左边为真返回左边,否则返回右边",and 相反,not 则恒返回布尔值,短路行为与 Java 一致。name or "匿名" 正是 Kotlin 里 Elvis 运算符 name ?: "匿名" 的对应物,Java 没有等价写法。要注意假值范围:空串、0、空列表、None 都会触发 or 兜底,业务上是否合意需要逐个确认。

6、海象运算符与序列乘法

:= 因形似海象得名,正式名称是赋值表达式,出自 PEP 572,Python 3.8 引入:

import re

text = "订单号:A12345"
if m := re.search(r"[A-Z]\d+", text):
    print(m.group())         # A12345:判断与捕获一步完成

values = [3, 1, 4, 1, 5]
if (n := len(values)) > 3:   # 边求值边绑定
    print(f"共 {n} 条数据")

解读:在表达式内部完成赋值是它的全部意义——条件求值的同时把结果留住,省掉"先赋值再判断"的两行。它优先级极低,嵌在比较里通常要加括号;能用普通赋值语句时就用语句,这也是 PEP 572 自己给的风格建议。这个运算符当年的争论激烈到创始人 Guido van Rossum 随后宣布卸任语言决策者职位,是 Python 历史上著名的争议。

运算符的另一半差异在重载。Python 的 + 与 * 由类型自己定义,序列类型就给了它们额外含义:

print("ab" * 3)        # ababab:字符串重复
print([0] * 3)         # [0, 0, 0]:列表重复
print((1, 2) + (3,))   # (1, 2, 3):元组拼接

解读:这些行为通过双下方法实现,自定义类定义 __add__、__mul__ 后同样能参与运算。Java 只给 String 开了 + 的后门,Python 则把运算符向所有类型开放。

7、整除估算 token 实战

回到开头那个例子。一段对话历史管理代码里常见的估算写法,先看第一步序列化:

import json

history = [{"role": "user", "content": "你好"}]
s = json.dumps(history, ensure_ascii=False)
print(s)  # [{"role": "user", "content": "你好"}] 中文原样保留
# 若不加 ensure_ascii=False,你好会转义成 4 个 \uXXXX 字符,串明显更长

解读:ensure_ascii=False 让中文直接输出原字符,既贴近人眼所见,也让长度估计不被转义膨胀干扰。序列化细节见 json 模块文档。

完整的估算与裁剪逻辑,示例如下:

MAX_TOKENS = 100_000

estimated = len(json.dumps(history, ensure_ascii=False)) // 2
if estimated > MAX_TOKENS:          # 粗判对话历史是否快超限
    history = history[1:]           # 先裁最旧一条,实际项目按策略裁剪

解读:len() 数的是字符个数,除以 2 是经验估算——中文与混合文本大约一到两个字符对应一个 token,取中间值足够粗判。这里用 // 而不是 /,因为 token 数要整数,粗略值向下取整即可。精确计数要么调用 tokenizer(慢、有依赖),要么等 API 响应里的 usage 字段(滞后),而上下文管理恰恰需要提前预判,零成本的估算反而是最优解。

8、总结

回顾要点:除法有三种,/ 真除、// 地板除、% 配套取余,负数上地板除与 int() 截断分道扬镳,% 符号跟随除数,这三点都与 Java 不同;没有 ++ --,循环交给 range;比较可以链式书写,判空用 is None;and 与 or 返回操作数本身,默认值惯用法要提防空串等假值;:= 在表达式里完成赋值,+ 与 * 可被类型重载。对于从 Java 迁移到 Python 的开发者,建议把负数除法的那张对照表抄进笔记,写循环时忘掉 i++,见到 x or 默认值 时先问一句空串该不该兜底——这三处是迁移期最集中的踩坑点。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

114