§ 陷阱清单
旧直觉,新坑
10 个迁移者高频翻车点。每个都有「坏味道 vs 地道写法」的对照和一句为什么 —— 出了诡异的 bug 先来这里对号入座。
可变默认参数在函数定义时只求值一次
Java · JS · Go 开发者高发def add_tag(tag, tags=[]):tags.append(tag)return tagsadd_tag("a") # ['a']add_tag("b") # ['a', 'b'] ← 不是 ['b']!
def add_tag(tag, tags=None):if tags is None:tags = []tags.append(tag)return tagsadd_tag("a") # ['a']add_tag("b") # ['b'] ✓
默认值表达式在 def 执行时求值一次并挂在函数对象上,所有调用共享同一个列表。这是 Python 头号面试题,也是 Java 迁移者写工具函数的高频 bug。
is 不是 ==:身份比较与值比较
Java 开发者高发a = [1, 2]b = [1, 2]if a is b: # False — 两个对象...if a == "x" is None: # 链式比较陷阱...
a = [1, 2]b = [1, 2]if a == b: # True — 值相等...if x is None: # is 只用于 None 单例...
Python 的 `==` 等价 Java 的 `equals()`,`is` 才等价 Java 的 `==`(比对象地址)。惯例:只有判断 None 用 is,其余一律 ==。
GIL:threading 不会加速 CPU 计算
Go · Java 开发者高发import threadingdef crunch():sum(i * i for i in range(10**7))threads = [threading.Thread(target=crunch)for _ in range(4)][t.start() for t in threads]# 耗时 ≈ 串行 ×1 — 全局解释器锁串行化
from multiprocessing import Pooldef crunch(_):return sum(i * i for i in range(10**7))with Pool(4) as pool: # 多进程真并行pool.map(crunch, range(4))# 或:让 NumPy / C 扩展去算(内部释放 GIL)
Go 的 goroutine 和 Java 线程都是真并行,Python 线程被 GIL 串行化。IO 等待会释放 GIL(所以 IO 并发有效),CPU 密集必须 multiprocessing。
裸 except 会吞掉一切
Java · JS 开发者高发try:process(order)except: # 吞掉 KeyboardInterrupt、SystemExit…pass # 静默失败,永远不知道出了什么
try:process(order)except (KeyError, TypeError) as e:logger.warning("处理失败: %s", e)raise # 处理不了就往上抛
裸 except 等价 catch (Throwable),还会捕获 Ctrl+C。永远写具体异常类型;捕获后要么处理要么 re-raise。
赋值是引用:复制要用显式操作
Go · Java 开发者高发a = [1, 2, 3]b = a # 只是引用别名b.append(4)print(a) # [1, 2, 3, 4]# 嵌套结构 copy 也不够:import copyc = copy.copy(a) # 浅拷贝
a = [1, 2, 3]b = a.copy() # 或 a[:] / list(a)b.append(4)print(a) # [1, 2, 3] ✓import copyc = copy.deepcopy(a) # 嵌套结构用深拷贝
和 Java 传引用一致,但 Python 没有值语义的 struct(Go 开发者注意)。一切皆对象引用,赋值从不拷贝。
闭包里的循环变量是晚绑定
JS 开发者高发fns = []for i in range(3):fns.append(lambda: i) # 都引用同一个 iprint([f() for f in fns])# [2, 2, 2] — JS 的 var 才会这样
fns = []for i in range(3):fns.append(lambda i=i: i) # 用默认参数固定print([f() for f in fns])# [0, 1, 2] ✓
Python 闭包变量是晚绑定(类似 JS 的 var),没有 let 的块级作用域。lambda i=i 立即求值一次作为默认参数。
缩进即作用域:if 里定义的变量外面可见
Java · Go 开发者高发if cond:result = compute()print(result)# Java 里编译报错;Python 里合法# — 但 cond 为 False 时 NameError
result = None # 先声明默认值if cond:result = compute()print(result)# 或者更好:用函数收拢作用域
Python 只有函数/模块作用域,没有块作用域。if/for 内的赋值会泄漏到外层 —— Java 开发者的直觉在这里失效。
循环里 += 拼接字符串是 O(n²)
Java 开发者高发parts = ""for p in items:parts += p + "," # 每次都新建字符串
parts = ",".join(items) # 一次构建# 必须循环时,先收集再 join:buffer = []for p in items:buffer.append(transform(p))out = ",".join(buffer)
字符串不可变(这点和 Java 相同),但你已经知道 StringBuilder —— Python 的对应物是 ''.join()。f-string 取代小规模拼接,join 取代循环拼接。
dict 遍历时增删会 RuntimeError
Java · Go 开发者高发for k in scores:if scores[k] < 0:del scores[k]# RuntimeError: dictionary changed size during iteration
# 方案一:遍历副本for k in list(scores):if scores[k] < 0:del scores[k]# 方案二(更地道):推导式重建scores = {k: v for k, v in scores.items() if v >= 0}
和 HashMap 并发修改类似,Python 把这个错误做成了运行时异常。原地过滤的答案永远是推导式。
动态类型到运行时才炸:用标注补回编译期
Go · Java 开发者高发def apply_discount(price, pct):return price * (1 - pct / 100)apply_discount("100", 10)# TypeError: unsupported operand…# — 但要到调用时才会发现
def apply_discount(price: float, pct: float) -> float:return price * (1 - pct / 100)# pyright 在编辑器里立刻标红:apply_discount("100", 10) # ✗ str 不能赋给 float
这是静态语言迁移者最深的失落感来源。解法不是换语言,是给公共函数加类型标注 + CI 跑 pyright —— 大半个编译器的检查就回来了。