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的循环导入