90%的Python程序员都踩过的8个代码坑,你中了几个? 文章目录前言坑一函数默认参数的幽灵——90%的Python开发者都踩过原因分析正确解法避坑技巧坑二深拷贝与浅拷贝的双胞胎——改一个另一个也变了原因分析正确解法避坑技巧坑三Python 3.7之后dict有序了OrderedDict就没用了大错特错先澄清一个误区OrderedDict仍然不可替代的3个场景1. 相等性判断依赖顺序2. 提供move_to_end()方法可以快速将元素移到开头或结尾3. 提供popitem(last...)方法可以选择从开头或结尾弹出元素避坑技巧坑四全局变量与局部变量的身份危机——UnboundLocalError原因分析正确解法避坑技巧坑五异常处理中的静默失败——程序崩了都不知道为什么原因分析正确解法避坑技巧坑六循环里的字符串拼接——你每次都在盖新房原因分析正确解法避坑技巧坑七切片操作的温柔陷阱——超出范围不报错返回浅拷贝原因分析正确解法避坑技巧坑八用检查类型而不是isinstance——继承的坑原因分析正确解法避坑技巧总结P.S. 目前国内还是很缺AI人才的希望更多人能真正加入到AI行业共同促进行业进步增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow教程通俗易懂高中生都能看懂还有各种段子风趣幽默从深度学习基础原理到各领域实战应用都有讲解我22年的AI积累全在里面了。注意教程仅限真正想入门AI的朋友否则看看零散的博文就够了。前言兄弟们先问个扎心的问题你写Python多少年了是不是觉得自己代码写得炉火纯青import用得行云流水AI插件一装一天能生成100个功能感觉自己马上就要晋升架构师了结果一到线上程序莫名其妙崩溃排查半天发现是个低级错误或者代码跑起来慢得像蜗牛优化了半天发现只是踩了个Python特有的坑我搞了22年AI写过的Python代码没有一百万行也有八十万行面过的候选人没有一千也有八百。我见过太多这样的人他们不是不努力也不是技术差而是被Python那些看似合理实则坑爹的特性给坑惨了。很多人觉得Python简单语法友好写起来快。但正是这种简单让很多人忽略了它背后的陷阱。有些坑新手踩了会一脸懵逼有些坑连工作五六年的老手都会翻车。今天我就把Python开发中最常见的8个代码坑整理出来每个坑都有真实的踩坑经历、错误代码、正确解法和避坑技巧。看完这篇文章你至少能少走3年弯路以后写代码再也不会被这些低级错误耽误时间了。坑一函数默认参数的幽灵——90%的Python开发者都踩过这绝对是Python中最著名、最坑人的一个特性没有之一。我敢说只要你写过超过100行Python代码就一定踩过这个坑或者即将踩这个坑。先看一段代码你觉得输出会是什么defadd_item(item,lst[]):lst.append(item)returnlstprint(add_item(1))print(add_item(2))print(add_item(3))很多人会想当然地认为输出是[1] [2] [3]但实际上运行结果是[1] [1, 2] [1, 2, 3]是不是很诡异为什么每次调用函数列表都会保留上一次的结果这不是bug这是Python的特性原因分析Python函数的默认参数在函数定义时只计算一次而不是在每次调用时计算。如果默认参数是可变对象如列表、字典、集合那么多次调用函数会共享同一个对象。这就好比你开了一家餐厅每次客人来吃饭你都给他们同一个盘子。第一个客人吃完后盘子里剩下了骨头第二个客人来你还是用这个盘子装菜结果菜里就有了上一个客人剩下的骨头。正确解法永远不要使用可变对象作为函数的默认参数。正确的做法是使用None作为默认值然后在函数内部创建可变对象defadd_item(item,lstNone):iflstisNone:lst[]lst.append(item)returnlstprint(add_item(1))# [1]print(add_item(2))# [2]print(add_item(3))# [3]避坑技巧函数默认参数只能使用不可变对象None、数字、字符串、元组如果需要默认值是可变对象一定要在函数内部初始化写代码时看到def func(a[]):这种写法直接打个问号这99%是个坑坑二深拷贝与浅拷贝的双胞胎——改一个另一个也变了这个坑我工作第三年还在踩而且踩得特别惨。当时我在写一个AI模型的数据预处理代码需要复制一份数据进行处理结果处理完发现原始数据也被改了导致模型训练结果完全错误排查了整整一天才找到原因。先看这段代码original[1,2,[3,4]]copy1original.copy()# 浅拷贝copy1[0]100copy1[2].append(5)print(原始数据:,original)print(拷贝数据:,copy1)你觉得输出会是什么很多人会说“原始数据不变拷贝数据被修改了。”但实际上运行结果是原始数据: [1, 2, [3, 4, 5]] 拷贝数据: [100, 2, [3, 4, 5]]看到了吗修改拷贝数据的外层元素原始数据不受影响但修改拷贝数据的内层嵌套列表原始数据也跟着变了这就是浅拷贝的陷阱。原因分析Python中的拷贝分为两种浅拷贝和深拷贝。浅拷贝创建一个新的外层容器但内部的元素仍然是原对象的引用。就像你复制了一个文件夹但文件夹里的文件还是原来的那些文件。你修改文件夹里的文件原来的文件也会被修改。深拷贝递归地复制所有层级的对象新对象与原对象完全独立。就像你不仅复制了文件夹还把文件夹里的所有文件都复制了一份。你修改新文件夹里的文件原来的文件不会受到任何影响。正确解法如果你的数据中有嵌套的可变对象一定要使用copy.deepcopy()进行深拷贝importcopy original[1,2,[3,4]]copy2copy.deepcopy(original)# 深拷贝copy2[0]100copy2[2].append(5)print(原始数据:,original)# [1, 2, [3, 4]]print(深拷贝数据:,copy2)# [100, 2, [3, 4, 5]]避坑技巧记住口诀“赋值共享浅拷外壳深拷全套”单层数据结构如普通列表、字典可以用浅拷贝嵌套数据结构如列表里套列表、字典里套字典必须用深拷贝不可变对象如元组、字符串的深浅拷贝没有区别坑三Python 3.7之后dict有序了OrderedDict就没用了大错特错这个坑是最近两年特别流行的很多人都被误导了。我面试的时候经常问这个问题90%的候选人都会说“Python 3.7之后dict已经有序了OrderedDict可以淘汰了。”每次听到这个回答我都会摇摇头。OrderedDict不仅没有被淘汰在某些场景下它还是不可替代的先澄清一个误区很多人以为Python 3.6的dict就已经有序了这是错误的。Python 3.6中dict的有序性只是CPython的实现细节不是Python语言的官方规范。也就是说如果你用的是PyPy、Jython等其他解释器3.6版本的dict依然是无序的。直到Python 3.7「dict保留插入顺序」才正式写入Python语言规范所有符合规范的解释器都必须实现这一特性。OrderedDict仍然不可替代的3个场景虽然普通dict现在也能保留插入顺序但OrderedDict还有3个普通dict没有的特性1. 相等性判断依赖顺序两个OrderedDict只有键值对完全相同且顺序一致才会判为相等而普通dict不关心顺序。fromcollectionsimportOrderedDict d1{a:1,b:2}d2{b:2,a:1}print(d1d2)# True普通dict不关心顺序od1OrderedDict({a:1,b:2})od2OrderedDict({b:2,a:1})print(od1od2)# FalseOrderedDict关心顺序2. 提供move_to_end()方法可以快速将元素移到开头或结尾这个方法在实现LRU缓存时特别有用odOrderedDict({a:1,b:2,c:3})od.move_to_end(a)# 将a移到最后print(od)# OrderedDict([(b, 2), (c, 3), (a, 1)])od.move_to_end(a,lastFalse)# 将a移到最前print(od)# OrderedDict([(a, 1), (b, 2), (c, 3)])3. 提供popitem(last...)方法可以选择从开头或结尾弹出元素普通dict的popitem()方法只能从结尾弹出元素odOrderedDict({a:1,b:2,c:3})print(od.popitem(lastTrue))# (c, 3)从结尾弹出print(od.popitem(lastFalse))# (a, 1)从开头弹出避坑技巧普通场景下直接使用普通dict即可性能更好内存占用更低如果需要显式强调顺序、或者需要使用move_to_end()和popitem(lastFalse)方法一定要用OrderedDict不要在Python 3.6及以下版本依赖dict的有序性这会导致跨平台兼容性问题坑四全局变量与局部变量的身份危机——UnboundLocalError这个坑我见过无数新手踩甚至有些工作两三年的开发者也会犯。先看这段代码count0defincrement():count1returncountprint(increment())你觉得输出会是什么很多人会说“1”。但实际上运行这段代码会报错UnboundLocalError: local variable count referenced before assignment是不是很奇怪我们明明在函数外面定义了count变量为什么函数内部说它没有被定义原因分析在Python中如果你在函数内部对一个变量进行赋值操作Python会默认将这个变量视为局部变量。即使函数外面有同名的全局变量Python也会忽略它。在上面的例子中count 1等价于count count 1。Python看到赋值操作就认为count是局部变量。但在执行count 1的时候局部变量count还没有被赋值所以就会报错。正确解法如果要在函数内部修改全局变量必须使用global关键字声明count0defincrement():globalcount count1returncountprint(increment())# 1避坑技巧尽量避免使用全局变量全局变量会让代码变得难以维护和调试如果必须使用全局变量在函数内部修改时一定要加global关键字不要使用与全局变量同名的局部变量这会导致变量阴影问题让代码变得非常混乱坑五异常处理中的静默失败——程序崩了都不知道为什么这个坑是最危险的因为它不会让程序直接崩溃而是会让程序带病运行导致更严重的问题。我曾经见过一个生产环境的bug就是因为一个裸except子句导致数据被错误地写入数据库造成了几十万的损失。先看这段代码defdivide(a,b):try:returna/bexcept:returnNoneprint(divide(10,2))# 5.0print(divide(10,0))# Noneprint(divide(10,2))# None看起来这段代码很健壮不管发生什么错误都不会崩溃。但实际上这是非常危险的写法原因分析裸except子句会捕获所有异常包括KeyboardInterrupt用户按CtrlC中断程序和SystemExit程序正常退出。这意味着如果你在程序运行时按CtrlC想退出程序程序可能会忽略你的操作继续运行。更严重的是裸except子句会掩盖所有错误。在上面的例子中divide(10, 2)会抛出TypeError但被裸except子句捕获了返回了None。你根本不知道是因为除数为0出错了还是因为参数类型错误出错了。正确解法永远不要使用裸except子句应该只捕获你预期的异常defdivide(a,b):try:returna/bexceptZeroDivisionError:returnNoneprint(divide(10,2))# 5.0print(divide(10,0))# Noneprint(divide(10,2))# 抛出TypeError这才是正确的行为避坑技巧永远不要写except:应该写except 具体异常类型:尽量捕获最具体的异常而不是宽泛的Exception捕获异常后一定要记录日志不要默默忽略错误只有在你确定要忽略所有异常的情况下才可以使用except Exception:但一定要加注释说明原因坑六循环里的字符串拼接——你每次都在盖新房这个坑是Python性能优化中最常见的一个很多人写了好几年Python都不知道。我曾经见过一个同事写的代码处理一个10万行的文本文件用了整整10分钟优化后只需要0.1秒。先看这段代码# 错误写法resultforiinrange(100000):resultstr(i)这段代码看起来很简单但实际上性能非常差原因分析Python中的字符串是不可变对象。当你执行result str(i)时Python并不会在原来的字符串后面追加内容而是会创建一个新的字符串然后把原来的字符串和新的内容复制到新的字符串中。这就好比你盖房子每次想加一个房间都要把原来的房子拆了重新盖一个更大的房子。盖10万次房子你说能不慢吗正确解法使用列表来收集字符串最后用join()方法拼接# 正确写法parts[]foriinrange(100000):parts.append(str(i))result.join(parts)join()方法会预先计算所有字符串的总长度然后一次性分配内存把所有字符串复制到新的内存中。这比每次都创建新字符串要快得多。避坑技巧永远不要在循环里使用拼接字符串使用列表收集字符串最后用join()拼接如果是简单的字符串拼接可以使用f-string性能也很好坑七切片操作的温柔陷阱——超出范围不报错返回浅拷贝Python的切片操作非常方便但也有两个容易被忽视的特性很多人踩了坑都不知道。先看这段代码nums[1,2,3,4,5]print(nums[10:20])# 你觉得会输出什么很多人会说会报错索引越界了。但实际上运行结果是[]Python的切片操作非常温柔当start或end超出列表范围时不会引发错误而是返回可用部分。这在某些情况下很方便但也可能导致逻辑错误。再看这段代码original[1,2,[3,4]]copyoriginal[:]# 切片操作copy[2].append(5)print(original)# 你觉得会输出什么很多人会说切片是拷贝原始数据不会变。但实际上运行结果是[1, 2, [3, 4, 5]]原因分析切片操作超出范围不报错这是Python的设计哲学“优雅胜于丑陋”。Python宁愿返回一个空列表也不愿意让程序崩溃。切片操作返回的是浅拷贝和list.copy()方法一样切片操作只会复制外层容器内部的元素仍然是原对象的引用。正确解法在使用切片操作前最好验证一下索引范围避免逻辑错误如果需要复制嵌套数据结构一定要使用copy.deepcopy()避坑技巧不要依赖切片操作的不报错特性这可能会掩盖你的逻辑错误记住切片操作是浅拷贝不是深拷贝如果你想复制一个列表original[:]和list(original)和original.copy()是等价的坑八用检查类型而不是isinstance——继承的坑这个坑很多老手都会踩特别是在处理继承关系的时候。先看这段代码classAnimal:passclassDog(Animal):passdogDog()print(type(dog)Dog)# Trueprint(type(dog)Animal)# Falseprint(isinstance(dog,Dog))# Trueprint(isinstance(dog,Animal))# True看到区别了吗type()函数返回的是对象的精确类型而isinstance()函数会考虑继承关系。原因分析在Python中继承是面向对象编程的核心特性之一。如果你的代码中使用了type() 来检查类型那么当有人继承了你的类并传入子类实例时你的代码就会出错。正确解法永远使用isinstance()来检查类型defprocess_animal(animal):ifnotisinstance(animal,Animal):raiseTypeError(必须传入Animal实例)# 处理动物避坑技巧永远不要使用type(x) T来检查类型应该使用isinstance(x, T)isinstance()可以同时检查多个类型isinstance(x, (int, float))只有在你确实需要检查精确类型的时候才可以使用type()但这种情况非常少见总结以上就是Python开发中最常见的8个代码坑。这些坑不是什么高深的技术难题但却能让你浪费大量的时间在调试上。我搞了22年AI写过无数的Python代码踩过的坑比你吃过的饭还多。我总结出一个道理写代码不是比谁写得快而是比谁写得稳。一个小小的bug可能会让你之前所有的努力都白费。希望这篇文章能帮你避开这些坑以后写代码更加得心应手。如果你觉得这篇文章对你有帮助别忘了点赞、收藏、关注我会继续分享更多AI和Python的干货。P.S. 目前国内还是很缺AI人才的希望更多人能真正加入到AI行业共同促进行业进步增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow教程通俗易懂高中生都能看懂还有各种段子风趣幽默从深度学习基础原理到各领域实战应用都有讲解我22年的AI积累全在里面了。注意教程仅限真正想入门AI的朋友否则看看零散的博文就够了。