Python 里那些踩过的坑

2026-11-05350 字Python

持续记录,遇到就写。


1. dict.get() 的默认值会被求值

# 看起来没问题,实际上 cost() 每次都会执行
value = cache.get(key, cost())

# 正确写法
value = cache.get(key)
if value is None:
    value = cost()

dict.get(key, default) 的 default 是函数参数,调用前就求值了。 如果 cost() 很贵,这就是个隐藏的性能炸弹。


2. 可变默认参数

# 错:所有调用共享同一个 list
def add(item, bucket=[]):
    bucket.append(item)
    return bucket

add(1)  # [1]
add(2)  # [1, 2]  ← 不是 [2]
# 对
def add(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

默认参数在函数定义时求值一次,之后一直复用。这是 Python 最经典的坑。


3. 闭包里的循环变量

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]   # [2, 2, 2]  不是 [0, 1, 2]

闭包捕获的是变量,不是值。循环结束时 i 是 2,三个 lambda 都看到 2。

# 用默认参数绑定当前值
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]   # [0, 1, 2]

4. is 和 == 混用

a = 256
b = 256
a is b   # True(小整数缓存)

a = 257
b = 257
a is b   # False(超出缓存范围)

CPython 缓存 -5 到 256 的整数。 is 只用来判断 None / True / False,其他一律用 ==。


5. 异常里的变量作用域

try:
    result = risky()
except Exception as e:
    pass

print(e)   # NameError: e 已被删除

Python 3 里,except ... as e 的 e 在 except 块结束时被自动删除。 需要留着就 err = e 存一份。


6. 文件读写忘了 with

# 出错时文件句柄泄漏
f = open('data.txt')
content = f.read()
f.close()   # 上面抛异常就到不了这里
with open('data.txt') as f:
    content = f.read()

with 保证异常时也关闭。文件、锁、连接都用它。


7. 排序不稳定(其实稳定,但 key 容易写错)

# 想要:先按年龄,年龄相同按名字
people.sort(key=lambda p: (p.age, p.name))

sort 是稳定排序,但如果你只写 key=lambda p: p.age, 同年龄元素的相对顺序取决于原列表顺序——这往往不是你想要的。

多字段排序就写元组。


8. + 拼字符串的性能陷阱

# O(n²):每次 + 都创建新字符串
s = ""
for x in items:
    s += x

# O(n):join 一次分配
s = "".join(items)

小数据无所谓,上百万次就是几秒 vs 几毫秒的差别。


9. 时间戳没带时区

# 错:naive datetime,不知道是哪个时区
now = datetime.now()

# 对:显式带时区
now = datetime.now(timezone.utc)

naive datetime 在不同机器上解释不同,跨时区就是 bug 温床。 从第一行代码就带 tz。


10. requirements.txt 没锁版本

# 危险
requests

# 安全
requests==2.31.0

不锁版本,哪天上游发新版改了行为,你的代码就莫名其妙挂了。 用 pip freeze > requirements.txt 生成。


待补充

  • [ ] asyncio 里的常见误用
  • [ ] dataclass 的 mutable default
  • [ ] __init__.py 的循环导入