尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python开发中的常见坑与规避技巧分享

Python开发中的常见坑与规避技巧分享 我见过太多自以为精通Python的人在凌晨三点的办公室里对着屏幕抓狂。他们要么用力敲击键盘要么死死盯着那几行一模一样的代码——代码没变变的只是他们内心的崩溃程度。外面流传着太多关于Python的赞美诗优雅、简洁、人生苦短。但这些都是幸存者偏差。真相是Python的每一分优雅背后都藏着一堆编排好的陷阱专门等着那些代码写得太顺的人。今天就把这些坑一个个挑出来说清楚别等到线上事故才想起来这篇文章。可变默认参数隐藏最深的幽灵你的代码写得毫无破绽但两次调用的结果完全不同。翻开函数定义你看到了一个空的list或dict。问题就出在这。def add_item(item, my_list[]): my_list.append(item) return my_list第一次调用返回[1]第二次调用返回[1, 2]第三次返回[1, 2, 3]。这太反直觉了。原因在于函数的默认参数只会在定义时被评估一次之后便永远持有同一个对象。Python不是每调用一次函数就给你创建一份新的默认值拷贝它只会把同一个对象递给你。规避方式直截了当默认值用None函数体内在判断。def add_item(item, my_listNone): if my_list is None: my_list [] my_list.append(item) return my_list这个坑极其隐蔽因为你可能几个月都不会触发它。但一旦触发排查起来费时费力而且年轻人往往最先怀疑自己的逻辑而不是怀疑这个默认参数。记住真正危险的不是代码复杂而是看上去过于简单的代码背后藏了一个活对象。闭包迟绑定循环里的一切都是假的你写了一个循环想在点击事件里获取不同变量结果运行时发现所有回调拿到的都是同一个值——最后一个值。funcs [] for i in range(5): funcs.append(lambda: i) for f in funcs: print(f()) # 输出全是 4全输出4不是0,1,2,3,4。这就是闭包迟绑定。Python的闭包捕获的是变量本身而不是变量的值。循环结束后i早已停在4这个位置所有lambda函数引用的都是同一个变量i取出来的自然全是4。闭包的绑定时机决定了你在循环里看到的永远不是当下而是终局。这坑难倒无数新手也让相当数量的老手栽过跟头因为很少人会直视循环变量来做闭包测试。修复办法把默认值作为参数传入人为制造一个绑定快照。funcs [] for i in range(5): funcs.append(lambda ii: i)只要多出ii这个默认参数就相当于每个lambda都捏紧了自己的那个i再也不会互相干扰。循环遍历时直接修改容器血流成河的现场需求很简单删掉列表里所有偶数。numbers [1, 2, 3, 4, 5, 6, 7, 8] for number in numbers: if number % 2 0: numbers.remove(number)结果不是[1, 3, 5, 7]而是[1, 3, 5, 7]你以为对了再去看看后面其实藏着[1, 3, 5, 7]是等一会儿的事。不对再认真跑一下你会发现结果是[1, 3, 5, 7]……别被绕进去了跑出来的实际结果会是[1, 3, 5, 7]我建议你立刻去终端验证别急着往后翻代码。很多教程在这个地方一笔带过但实际出错时你会陷入一个离奇的漩涡明明删了偶数剩下来的居然还是偶数成员。真正的结果是列表里有些偶数被你跳过了。原因在于你在迭代列表的同时直接对它做remove操作这会让列表的索引结构发生变化。当你删除一个元素后后面的元素会补位向前而循环迭代器手里的“当前位置”指到下一个索引。这就造成了一个偶数元素被补位到前一个位置上却因为索引已经往后走而被完美跳过。这就是修路和过路同时进行路永远修不完车永远过不去。解决办法别啰嗦列表推导式一行清理numbers [n for n in numbers if n % 2 ! 0]只此一行逻辑清晰、观感清爽、风险为零。别去碰remove边遍历的老问题不要给自己挖坑。字典遍历时删除条目直接炸掉你的运行时这个坑比上一个更危险。Python允许你在遍历字典的过程中删除元素吗语法上允许但运行时直接报RuntimeError: dictionary changed size during iteration。my_dict {a: 1, b: 2, c: 3} for k in my_dict: if k b: del my_dict[k]这行代码必定抛出异常。为什么字典是一种哈希表删除键值会改变内部的哈希表大小或迭代器的位置判断导致迭代器失效。这个和列表修改还不太一样字典直接翻脸。迭代器的世界里修改容器的结构是一种背叛。别指望它宽容一次它会直接让程序崩溃。正确处理姿势依然简单my_dict {k: v for k, v in my_dict.items() if k ! b}或者在遍历前先拷贝一份键列表for k in list(my_dict.keys()): if k b: del my_dict[k]别觉得多写一点代码烦。写代码不怕啰嗦怕的是在深夜里问自己怎么会崩崩溃的答案往往就是你在不该遍历修改的时候动了手。深浅拷贝分不清一半数据莫名其妙被改Python里赋值、引用、拷贝、修改这一连串概念坑过的绝对不止你一个。a [[1, 2, 3], [4, 5, 6]] b a[:] b[0][0] 999你以为a还是原样不a[0][0]也变成了999。这完全违背直觉切片明明是生成了一个新列表对吧是生成新列表了但新列表的元素却还是对原内部列表的引用这就是浅拷贝。浅拷贝只复制了一层外壳里面的核心还是共享的。当你做数据复制、列表切片、字典copy时默认走的都是浅拷贝。真实开发场景中最典型的现象就是你修改了一个“副本”上游的数据源却莫名其妙跟着变了排查了半天还以为是数据源出了岔子。正确方式是显式深拷贝import copy b copy.deepcopy(a)但这里有个隐藏性能坑深层拷贝对于大对象非常昂贵。不要无脑拷贝。要分清楚哪些是只读共享哪些需要完全隔离。拷贝不是免费的但没有拷贝时数据污染会毁掉你一整天。浮点数比较你以为相等就相等0.1 0.2 0.3返回什么看起来再显然不过的问题答案是False。是的你猜对了。Python浮点数遵循IEEE 754规范二进制无法精确表示某些十进制小数。0.1在二进制中是一个无限循环小数。这个坑不新鲜但它害起人来毫不含糊。尤其是数据处理、金融计算、价格计算等领域一旦你拿浮点数直接比较大小结果就可能不符合业务预期。在二进制的世界里没有一个浮点数天生无辜。规避方式有讲究。比较时用math.isclose()而不是。涉及金额时直接用Decimal类型并且初始值用字符串而不是浮点数。from decimal import Decimal price Decimal(0.1) Decimal(0.2) assert price Decimal(0.3)如果只是日常判断误差范围math.isclose足够。别抱着round函数不放round有自己的银行家舍入行为老坑了等你踩。继承层级陷进钻石继承的初始化混乱Python的多重继承机制MROMethod Resolution Order是一个充满奇技淫巧的领域。看起来简单的多继承逻辑实际跑起来调用顺序会让人目瞪口呆。常规情形下C3线性化算法能处理复杂的继承拓扑。但一旦你的类图出现了菱形结构super().__init__()的调用链路就会发生你意想不到的变化。比如A是基类B和C都继承AD同时继承B和C。如果你在D里调用super().__init__()实际执行的是B的初始化然后B的super转到C的初始化最后才是A的。也就是说每个类都只被初始化一次但它的执行顺序和你的直觉完全不同。多重继承问题的本质不是你调用谁而是你不知道下一个是谁。很多人建议用mixin来替代多重继承这话没错但更核心的坑在于不要在多个父类里都写__init__并依赖super链路除非你把每个类都当mixin设计成乐高积木。必要时直接在__init__里显式调用父类初始化方法别让super替你猜。异常捕获太宽泛把所有错误都吞进肚子try...except...是防御性编程的标配但很多人写得像段子try: # 任何可能出错的逻辑 do_something() except Exception: pass这段代码的含义简直可怕它把每一处失败都当成成功来表演。程序不会崩溃但它会在错误的上层继续运行直到产生不可预知的惨案。到时候你连错误发生的位置都不知道因为已经被pass吞了。永远不要空except永远不要在捕获异常后什么都不做。捕获异常至少要logging.error记录下来。最好精确捕获具体的异常类型比如KeyError、ValueError,留给别的异常继续向上抛。try: result risky_operation() except (KeyError, ValueError) as e: logging.error(特定错误发生: %s, e) else: # 异常未发生时才执行的逻辑 pass异常处理的目的不是防崩溃而是让崩溃地点和原因变得可见。你的程序不会因为一个盲目的except变得更强健只会变得更加无法排查。全局变量和状态污染隐式依赖是灾难的源头很多Python初学者热爱全局变量。程序状态散落各处模块之间互相修改同一个可变对象——这种代码往往写着爽维护起来像在腐烂的泥潭里打滚。一个函数改了全局变量另一个模块的某段逻辑依赖了这个全局变量第三个模块再修改它最终你在两个相差十万八千里的函数之间建立了一条隐秘的耦合关系。当程序出现瑕疵时你根本不知道是哪个点把状态改成错误的。函数的纯度越高代码的可维护性越强。状态越集中头脑越清醒。能通过参数传入就绝不读取全局能通过返回值传递就绝不修改外部状态。如果确实需要共享状态用类或者上下文管理器把状态的边界画清楚而不是让每个模块都能改了全局变量一走了之。用字符串拼接SQL黑客最爱你的代码这是一个已经被说烂了但仍然频繁出现的经典大坑。user_id request.args.get(uid) sql SELECT FROM users WHERE id user_id ; cursor.execute(sql)只要user_id输入一些特殊字符比如1 OR 11整张表的数据就尽收眼底。这不是Python特有的问题但Python的开发效率和大量的Web框架让这种写法的出现频率格外高。SQL注入的本质不是代码没写对而是你把用户输入当成了代码执行。数据永远只是数据代码永远只是代码两者必须严格分离。参数化查询是唯一正确姿势sql SELECT FROM users WHERE id %s cursor.execute(sql, (user_id,))任何ORM框架、数据库驱动都支持这个机制。此外还有人喜欢用f-string拼SQL那个痛快啊但是当线上数据被脱裤的时候那个痛快就会变成痛苦。避开这个坑的唯一手段就是永远不要字符串拼接SQL语句。依赖版本更新环境爆炸一夜之间Python环境依赖管理本身就是一个大坑。今天装的新包把旧包的API改得面目全非明天pip install一个依赖把整个环境启动依赖全升级了你的代码瞬间从运行正常变成报错满天飞。这种“环境漂移”问题在多人协作、多服务器部署时尤其致命。开发机跑得好好的一到生产环境就崩。究其原因往往是生产环境里的依赖版本和开发环境不一致。锁版本就是锁命能用requirements.txt里精确到就绝不写。更高级一点用pip-tools或poetry锁定依赖解析结果生成一个完整锁定文件提交到仓库。虚拟环境是基本功每个项目一套环境不在全局环境里瞎装包。这个坑的本质是很多人把“能用就行”当成了部署指南。等到某天一个依赖包悄无声息地更新你的系统从此在午夜时分开始报错届时你才明白版本锁定这四个字的价值。性能误判越炫的写法越慢Python有一些让人赏心悦目的写法看起来简洁高级实际运行起来却慢得吓人。比如用拼接大量字符串循环里反复调用同一个属性访问在列表里用in做大规模检查。result for chunk in many_chunks: result chunk如果many_chunks是一个大列表这段代码会反复创建新字符串产生大量内存拷贝和时间开销。正确写法是.join(many_chunks)。再比如用列表做成员检查if user_id in user_id_list: # O(n)当user_id_list有上百万条数据时这个检查慢得可怕。换成set或dict直接O(1)。优雅不应该是性能的敌人性能也不应该成为优雅的牺牲品。Python的核心优势之一是开发速度快但写代码时同样要关心算法复杂度否则一旦数据量上来开发时省的那点时间运行时要连本带利还回去。隐藏递归入口栈溢出无预警递归在Python中需要格外小心。默认的递归深度限制大约1000层sys.getrecursionlimit()超过就抛出RecursionError。很多递归场景看似没问题但实际的数据规模会超过限制比如深度遍历一个大型JSON结构、递归展开一个树状菜单。你不在意的边界最后往往就是你崩溃的那一道。规避方式递归前先评估最大深度超过sys.getrecursionlimit()就得改循环或者用sys.setrecursionlimit但不要依赖它毕竟每一层递归都会消耗调用栈栈是内存内存是有限度的。所有的递归终将消亡。区别只是被谁终止你、数据规模、还是系统内存。最佳方案是把递归模拟成显式栈stack [root] while stack: node stack.pop() # 处理逻辑 stack.extend(node.children)这套写法看似朴素但做大规模遍历时稳如老狗。写在代码之外Python开发中的坑远不止这些。每个坑背后都有一批人用熬夜和线上事故换来了血泪教训。有些坑来自语言本身的特性有些坑来自人对特性的误解。不管哪一类都指向同一个事实真正考验你的不是你写了多少优雅的精妙代码而是你能在踩坑多少年后还记得每一个坑在哪条路上。这世上没有不踩坑的程序员但有在同一个坑里反复掉进去的人和踩过一次就把坑填平的人。后者并没有更多天赋只是多花了一点时间理解底层机制。Python的核心哲学是“让开发者能够快速表达想法”但它并没有承诺替你规避掉那些反直觉的阴暗角落。别怕坑但要记得每一次你用列表推导式取代循环每一次你用深拷贝取代浅拷贝每一次你精确捕获异常而不是盲目吞掉你都在悄悄改变代码的下限。最终你写在代码里的不仅仅是逻辑而是你在无数次崩溃和纠错中凝练出来的判断力。今晚写下的每一行代码未必都有回报但每一次防备好一个坑都会让明早的生产环境更安静一点。
返回列表