写了几年 Python,总有几个瞬间让人怀疑人生——逻辑反复推演过,自认天衣无缝,一运行结果却南辕北辙;调试到凌晨三点,发现罪魁祸首竟是 Python 某个“贴心”的设计特性。这些坑不算语言缺陷,更多是“设计上的反直觉”——新手容易踩,老手稍不留神也会栽进去。本文梳理 Python 开发中最常见的几类陷阱,从根源拆解,给出可落地的规避方案。
缩进不是风格,是语法
从 Java、C++ 转来的开发者最容易在这件事上栽跟头。在其他语言里,缩进关乎美观;在 Python 里,缩进直接决定代码能否运行。同一代码块缩进不一致,或空格与 Tab 混用,解释器会毫不客气地抛出IndentationError。
解决方案简单但必须严格执行:全程使用4 个空格作为缩进(PEP 8 官方推荐)。在 VS Code 或 PyCharm 中开启“显示空白字符”,将编辑器 Tab 键自动转换为空格。写代码时边写边运行,不要攒了一大坨再回头调试——那时缩进错在哪一行,连自己都看不出来。
“==”和“is”,一字之差,谬以千里
这是新手最容易被迷惑的地方。==比较的是值是否相等;is比较的是两个变量是否指向同一块内存地址。
a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True,值相同 print(a is b) # False,两块不同的内存
更隐蔽的是小整数池的干扰:Python 对 -5 到 256 的整数做了缓存,导致is有时返回 True 有时返回 False,让人摸不着头脑。铁律只有一条:判断值是否相等,永远用==;仅在判断变量是否为None或单例时用is。
可变默认参数——最著名的“记忆陷阱”
函数默认参数在定义时被创建和计算,而非每次调用时重新创建。这意味着:
def add_item(item, items=[]): items.append(item) return items print(add_item('a')) # ['a'] print(add_item('b')) # ['a', 'b'] —— 预期是 ['b']同一个列表被所有调用共享,数据像滚雪球一样越积越多。线上曾有同事用可变默认参数记录日志 trace_id,结果一条日志里塞了十几个 id,直接变成面条。
解决方案:将默认值设为None,函数内部再初始化。
def add_item(item, items=None): if items is None: items = [] items.append(item) return items
闭包的延迟绑定——循环里的 lambda 为何全是 9?
funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) # 输出 [2, 2, 2],不是 [0, 1, 2]
Lambda 函数不会在创建时“记住”i的值,而是在被调用时才去环境中查找i的当前值。循环结束时i已经是 2,所以三个函数都返回 2。
解决方案:用默认参数“冻结”当前值。
funcs = [lambda i=i: i for i in range(3)] # 默认参数在定义时绑定
浮点数精度——0.1 + 0.2 不等于 0.3
print(0.1 + 0.2) # 0.30000000000000004 print(0.1 + 0.2 == 0.3) # False
这不是 Python 的 bug,而是所有编程语言都面临的二进制浮点数先天缺陷。金融、计费、金额计算等场景,永远不要用浮点数。使用decimal模块,或直接用整数分(如 100 分 = 1 元)存储。
迭代中修改序列——越改越乱
在遍历列表时删除元素,或在遍历字典时增删键,都会引发灾难。
nums = [1, 2, 3, 4, 5] for num in nums: if num < 5: nums.remove(num) # 列表长度变化,索引错乱 print(nums) # 输出 [2, 4, 5],完全错误[reference:34]
字典遍历时增删键会直接抛出RuntimeError: dictionary changed size during iteration。
解决方案:迭代副本,修改原列表。
for num in nums.copy(): if num < 5: nums.remove(num)
字典场景用字典推导式创建新字典,而非在原字典上边遍历边修改。
裸 except——最安静的炸弹
try: risky_operation() except: # 捕获所有异常,包括 KeyboardInterrupt、SystemExit pass # 静默失败[reference:38]
裸露的except:会捕获所有异常,包括你根本不该吞掉的键盘中断和系统退出信号。线上排查问题时,错误被静默吞掉,连日志都没留下,堪称最安静的定时炸弹。
最佳实践:永远指定具体的异常类型。
try: risky_operation() except ValueError as e: logger.error(f"值错误: {e}") raise写在最后
这些陷阱的共同特征是:看似符合直觉,实则背离 Python 的设计语义。避开它们不需要多高深的技术,只需要两样东西:一是对 Python 核心机制(可变与不可变、作用域与闭包、求值时机)有清晰的认识;二是养成写代码时多问一句“这里真的如我所想吗”的习惯。毕竟,bug 最可怕的地方从来不是它有多复杂,而是它看起来多么“理所当然”。