Python开发中常见的10个错误及解决方法
你会以为Python简单到不会犯错等踩过这些坑再说话。错误1把可变对象当作函数默认参数几乎每个Python新手都会栽在这里。看这段代码def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] ← 惊人的结果默认参数在函数定义时只被计算一次之后每次调用都复用同一个列表对象。第二次调用时lst已经是[1]再追加2自然得到[1,2]。这不是你想要的“每次调用都生成空列表”的行为。解决方法永远用None作为默认值在函数内部判断并创建新对象。def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst这一条足以让你在团队Code Review里被反复提醒。记住可变默认参数是Python最隐蔽的陷阱之一它悄无声息地破坏函数的纯性。错误2误解变量作用域——泄露的局部变量与意外的全局引用当你在函数内部直接给一个变量赋值Python默认把它当作局部变量。但如果你先引用再赋值就会触发UnboundLocalError。count 0 def increment(): count 1 # UnboundLocalError: local variable count referenced before assignment你认为count是全局的但Python看到赋值操作也是赋值后就认为count是局部变量而此时它还没有被定义。解决方法使用global关键字显式声明。更推荐用函数参数和返回值来避免全局依赖def increment(c): return c 1另一个常见误区是在嵌套函数中引用外层变量时内层函数对外层变量的赋值同样会创建新的局部变量。闭包里的延迟绑定更是经典错误。funcs [lambda x: x i for i in range(3)] print([f(2) for f in funcs]) # [4, 4, 4] 不是 [0, 2, 4]因为i在循环结束后才被求值此时它已经是2。lambda表达式捕获的是变量引用不是创建时的值。修复方法是使用默认参数lambda x, ii: x i。错误3except:裸捕获与pass的沉默杀机try: risky_operation() except: pass这段代码吞掉了所有异常包括KeyboardInterrupt、SystemExit甚至内存错误。当你试图用CtrlC终止程序时中断被优雅地忽略你只能强行杀死进程。裸except等于告诉Python任何错误都不重要继续执行。这在生产环境里是灾难——你可能永远不知道数据库连接失败、文件缺失、API调用超时。解决方法要么指定异常类型要么至少记录日志try: risky_operation() except Exception as e: logging.exception(操作失败: %s, e) # 不要用 print用 logging如果确实需要处理多种异常用元组列出它们except (ValueError, TypeError) as e。永远不要用裸except除非你在最顶层做全局兜底并且有日志。错误4混淆is与——不只是性能问题a 256 b 256 print(a is b) # True (小整数缓存) c 257 d 257 print(c is d) # False (对象不同)is比较内存地址比较值。绝大多数时候你应该用只有判断单例如None、True、False时才用is。但很多人误以为is是更快的“等于”在比对字符串或数字时使用结果因为Python的对象缓存机制时而正确时而错误。更危险的是对可变对象的判断x [1, 2, 3] y [1, 2, 3] print(x is y) # False print(x y) # True把is当作来用会让你的代码在逻辑上随机出错。永远牢记is是身份是等价。错误5在迭代列表时修改列表——索引偏移灾难numbers [1, 2, 3, 4, 5] for num in numbers: if num % 2 0: numbers.remove(num) # 删除偶数 print(numbers) # [1, 3, 5]? 实际上可能是 [1, 3, 4, 5]当删除元素时列表长度变化后续元素的索引前移导致循环跳过一些元素。上面的例子中删除2后3的索引变成1但循环指针已经移到了索引2原先是4所以3被跳过。解决方法创建新列表列表推导式或反向迭代numbers [n for n in numbers if n % 2 ! 0] # 推荐 # 或者 for num in numbers[:]: # 遍历副本 if num % 2 0: numbers.remove(num)迭代时修改容器是Python程序员最容易犯的结构性错误类似问题也出现在字典不能迭代时增删键和集合中。错误6忽视深拷贝与浅拷贝的差异当你写new_list old_list时你只是复制了引用。修改new_list会同时影响old_list。很多人知道这个所以用copy.copy()但遇到嵌套列表时又出错import copy original [[1, 2], [3, 4]] shallow copy.copy(original) shallow[0].append(5) print(original) # [[1, 2, 5], [3, 4]] —— 被改变了因为浅拷贝只复制了外层容器内部元素仍然是同一对象。默认的list()构造、dict()构造都是浅拷贝[:]切片也是。解决方法用copy.deepcopy()完全复制对象的整个结构。但注意深拷贝可能很慢且对某些循环引用会抛出异常。当你需要独立副本时先想清楚你需要浅拷贝还是深拷贝不要默认用copy.copy()。错误7字符串拼接导致性能雪崩result for i in range(100000): result str(i)Python中字符串不可变每次都会创建一个全新的字符串对象复制旧内容并追加新字符。这种二次方时间复杂度的操作会让处理数千个字符串的程序从毫秒级变成分钟级。解决方法用str.join()或列表推导result .join(str(i) for i in range(100000)) # O(n)或者使用io.StringIO在构建大文本时。记住在循环里做字符串加法是最昂贵的Python反模式之一它在初学者的代码里反复出现。错误8误解GIL导致的多线程性能幻觉你写了多线程来加速CPU密集型任务却发现比单线程还慢。因为CPython的全局解释器锁GIL保证了同一时刻只有一个线程执行Python字节码。对于计算密集型任务多线程其实是串行化加上线程切换开销性能反而下降。import threading def heavy_work(): sum(i 2 for i in range(10000000)) # CPU bound threads [threading.Thread(targetheavy_work) for _ in range(4)] for t in threads: t.start() for t in threads: t.join()这不会比单线程快更可能慢30%左右。解决方法区分I/O密集型和CPU密集型。I/O密集型用多线程因为I/O等待时GIL释放CPU密集型用多进程multiprocessing或使用concurrent.futures.ProcessPoolExecutor。另外使用NumPy、C扩展或asyncio也可以绕开GIL限制。GIL不是不可逾越但你必须知道何时需要绕过它。错误9忽略文件描述符的释放——with语句是救星f open(data.txt, r) data f.read() # 没有调用 f.close()即使你记得close()如果在读取过程中发生异常close()将被跳过导致文件句柄泄露。打开的文件数有上限积累后程序无法再打开新文件。更糟的是写入模式未关闭可能导致数据丢失因为缓冲区未刷新。解决方法永远使用上下文管理器withwith open(data.txt, r) as f: data f.read() # 离开 with 块自动关闭即使异常也关闭同样的原则适用于所有需要释放的资源锁、数据库连接、网络socket。with语句是Python最优雅的资源管理工具不用它几乎是一种渎职。错误10混淆与in——自以为是的成员判断if value in my_list: # do something很多人写if value in my_list时潜意识里认为in会对每个元素调用__eq__进行相等比较这没问题。但他们会写成if value my_list价值错误或者更隐蔽的if error in response: # 以为 response 是字符串或列表实际上是字典in在字典上检查的键不是值。而如果要检查字符串子串abc in aabbcc返回True但可能不是你要的精确匹配。另一个经典错误对生成器使用in生成器一旦消耗就没了gen (x for x in range(10)) if 5 in gen: # 消耗了直到5 print(list(gen)) # [6, 7, 8, 9]in操作符的行为取决于右边的容器类型它不是一个万能的存在性检查。在调试时先确认右边对象的__contains__方法逻辑。这些只是冰山一角。很多Python错误都源于语言的动态特性和隐式约定。正确使用Python需要你理解它的设计哲学——显式优于隐式可读性优先以及永远不要低估沉默的异常。避开这10个陷阱你的Python代码就已经超越了80%的开发者。剩下的20%则来自持续不断的实际项目锤炼和对语言底层机制的深挖。