PyPyBridge

§ 陷阱清单

旧直觉,新坑

10 个迁移者高频翻车点。每个都有「坏味道 vs 地道写法」的对照和一句为什么 —— 出了诡异的 bug 先来这里对号入座。

P01

可变默认参数在函数定义时只求值一次

Java · JS · Go 开发者高发
坏味道
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
add_tag("a") # ['a']
add_tag("b") # ['a', 'b'] ← 不是 ['b']!
地道写法
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
add_tag("a") # ['a']
add_tag("b") # ['b'] ✓

默认值表达式在 def 执行时求值一次并挂在函数对象上,所有调用共享同一个列表。这是 Python 头号面试题,也是 Java 迁移者写工具函数的高频 bug。

P02

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,其余一律 ==。

P03

GIL:threading 不会加速 CPU 计算

Go · Java 开发者高发
坏味道
import threading
def 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 Pool
def 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。

P04

裸 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。

P05

赋值是引用:复制要用显式操作

Go · Java 开发者高发
坏味道
a = [1, 2, 3]
b = a # 只是引用别名
b.append(4)
print(a) # [1, 2, 3, 4]
# 嵌套结构 copy 也不够:
import copy
c = copy.copy(a) # 浅拷贝
地道写法
a = [1, 2, 3]
b = a.copy() # 或 a[:] / list(a)
b.append(4)
print(a) # [1, 2, 3] ✓
import copy
c = copy.deepcopy(a) # 嵌套结构用深拷贝

和 Java 传引用一致,但 Python 没有值语义的 struct(Go 开发者注意)。一切皆对象引用,赋值从不拷贝。

P06

闭包里的循环变量是晚绑定

JS 开发者高发
坏味道
fns = []
for i in range(3):
fns.append(lambda: i) # 都引用同一个 i
print([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 立即求值一次作为默认参数。

P07

缩进即作用域: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 开发者的直觉在这里失效。

P08

循环里 += 拼接字符串是 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 取代循环拼接。

P09

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 把这个错误做成了运行时异常。原地过滤的答案永远是推导式。

P10

动态类型到运行时才炸:用标注补回编译期

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 —— 大半个编译器的检查就回来了。