在 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 默认值 时先问一句空串该不该兜底——这三处是迁移期最集中的踩坑点。

