尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python新手必看:10个高频坑一次讲透,缩进、默认参数、深拷贝全解析

Python新手必看:10个高频坑一次讲透,缩进、默认参数、深拷贝全解析 直接说结论Python 是一门上手极快的语言我见过很多零基础的同学第一周就能写出能跑的小脚本但恰恰是这种低门槛让大家忽略了一些底层机制等代码量上来之后一行诡异的报错可能要查上半天。这篇文章整理了 10 个新手最容易踩的坑覆盖缩进、变量、类型、函数、容器、异常这几大类每个都配了错误示例、正确写法和原理说明。不管你是刚装好 Python 准备学语法还是已经写过几百行代码开始接触函数和类这篇文章都值得你花十分钟过一遍能帮你省下不少排查 bug 的时间。先说清楚Python 的报错信息已经算非常友好的了很多坑其实有迹可循。但有些坑藏得很深比如明明没报错但结果就是不对这种比直接报错更让人头大。我下面列的这些坑基本都是我在实际写代码、帮别人看代码过程中反复遇到的不搞那些生僻刁钻的冷知识全是最日常的场景。1. 缩进问题看着对实际不对1.1 Tab 和空格混用报错却藏得深Python 用缩进表示代码块这一步劝退了很多人。但更隐蔽的问题是 Tab 和空格混用。很多编辑器默认把 Tab 显示成 4 个空格肉眼根本分辨不出来所以代码看起来对齐得整整齐齐一运行就报IndentationError: unexpected indent。我之前带过一个新人他的代码里前几行用空格缩进后面某一行用了 Tab编辑器里看着完全正常但一执行就报错。这种问题在团队协作里特别容易出现因为每个人编辑器配置不一样有人把 Tab 自动替换成 4 空格有人保留 Tab。最稳的做法是统一用空格并且在编辑器里开启显示空白字符功能。VS Code 里设置editor.renderWhitespace: allPyCharm 里在 Settings - Editor - General - Appearance 里勾选 Show whitespaces。这样 Tab 和空格就能一眼分辨。1.2 多一层缩进导致逻辑悄悄变化还有一种更隐蔽的情况缩进不报错但逻辑错了。比如if score 60: print(及格) print(继续加油)这段代码两个 print 都在 if 里面。但如果第二个 print 没对齐变成if score 60: print(及格) print(继续加油)代码不报错但继续加油不管多少分都会执行。这种 bug 不会给你任何提示纯粹靠肉眼找。排查思路是在 IDE 里右键选择 Format Document或者手动选中代码块按重新缩进快捷键把缩进统一后逻辑结构就一目了然了。注意写 Python 一定要强迫自己用 4 空格缩进不要用 2 空格更不要混用。这个习惯从第一天就要养成不然后面写类、写函数、写循环的时候缩进层级一多必乱。2. 可变对象当默认参数函数第二次调用就变味2.1 默认参数是共享的不是每次新建这是一个非常经典的坑。看这段代码def add_item(item, items[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2]第二次调用返回了[1, 2]而新手直觉上会觉得应该返回[2]。原因在于Python 函数的默认参数只在函数定义的时候创建一次之后每次调用如果不传该参数用的都是同一个列表对象。这个列表在多次调用之间一直被保留着。正确的写法是def add_item(item, itemsNone): if items is None: items [] items.append(item) return items2.2 为什么 None 就没问题None 是不可变对象每次函数调用时传入的默认参数都是同一个 None但你在函数内部对 items 重新赋值了所以不会影响到下一次调用。这个模式在源码里随处可见比如处理配置、缓存、收集结果时凡是默认参数可能被修改的场景都要用 None 占位再内部初始化。我个人的习惯是凡是默认参数是列表、字典、集合的一律先写 None再在函数体里判断。这种写法已经写进肌肉记忆了不需要思考。3. 变量作用域函数里改不到外面的变量3.1 赋值即声明局部变量遮蔽全局变量新手写代码经常遇到这种情况count 0 def increment(): count 1 increment() print(count)直接报UnboundLocalError: local variable count referenced before assignment。原因不是 Python 不让函数改全局变量而是在函数体内任何对变量赋值的操作都会让 Python 把这个变量视为局部变量。count 1其实分两步先读 count再写 count。但 Python 在编译函数时发现 count 在函数体内被赋值了直接把它标记为局部变量于是读的时候就发现它还没有被赋值。解决方案有三种第一用global声明def increment(): global count count 1第二把 count 作为参数传入用返回值接收def increment(count): return count 1 count increment(count)第三把可变对象作为容器比如列表或字典修改其内容不需要 global。3.2 嵌套函数的 nonlocal 陷阱在闭包里也有类似问题def outer(): x 10 def inner(): x 1 return x return inner一样的报错需要用nonlocal x声明。这个知识点在写装饰器、写回调函数的时候特别容易碰到。我的建议是尽量避免在函数里修改外部变量能返回值就用返回值代码会更清晰也更好测试。全能型资深博主4. 类型比较用is而不是4.1 is 比的是身份 比的是值这是非常高频的一个错误。新手学了is和都能用来比较于是写出来这种代码a input(输入: ) if a is hello: print(匹配)看起来没毛病在 Python 里is比较的是两个变量是否指向同一个对象比较的是两个变量的值是否相等。输入得到的字符串和代码里的字符串常量可能是两个不同的对象所以这个判断很可能永远不成立。正确的写法if a hello: print(匹配)4.2 什么时候该用 isis最常见的正确用法是判断是否为空比如if x is None。因为 None 在 Python 里是单例对象所有 None 指向的都是同一个地址所以用is判断效率更高也更符合语义。还有布尔值 True/False 也可以用 is 判断但一般没必要。有一个比较坑的地方是某些场景下is看起来也能工作比如a 1 b 1 print(a is b) # True这是因为 CPython 对小整数通常是 -5 到 256做了缓存a 和 b 都指向缓存里的同一个对象。但如果你换成 257结果就是 False。这种表现极其容易让人误解为 is 可以用于数值比较千万不要这么写。5. 整数除法和类型转换的坑5.1 Python 2 遗留习惯与 Python 3 的行为差异如果你用的是 Python 35 / 2得到的是2.5这是正常的浮点数除法。但如果你是想取整写了5 / 2却得到 2.5然后赋值给一个需要整数的场景就会出问题。我知道有些同学是从 Python 2 转过来的Python 2 里5 / 2得到的是2整数除法直接截断。到了 Python 3这个行为变了/永远是浮点除法//才是整数除法。写代码的时候要明确自己的意图# 浮点数除法 result 5 / 2 # 2.5 # 整数除法向下取整 result 5 // 2 # 2 # 取余数 result 5 % 2 # 1有一个容易被忽略的细节//是向下取整不是向零取整。对正数来说没有区别但对负数来说-5 // 2的结果是-3因为向下取整是往更小的方向取。如果业务逻辑需要向零取整要用int(-5 / 2)。5.2 类型转换的隐性坑字符串转整数的报错很常见比如int(12.5)直接报ValueError。新手容易疑惑12.5 不就是数字吗但 int 要求字符串必须是整数的字符串表示不能带小数点。如果要转浮点要先 float 再 int或者直接int(float(12.5))。再看一个常见的场景从文件或接口读到的数字往往是字符串类型比如 3 和 5 做加法直接报TypeError: unsupported operand type(s) for : int and str。这个报错信息很明确新手只要看到 TypeError 就去查类型基本都能解决。6. 遍历列表时正着删除元素从眼前溜走6.1 删除导致索引错位这个坑几乎每个人都会踩一次。需求是删除列表里所有偶数nums [1, 2, 3, 4, 5, 6] for i in range(len(nums)): if nums[i] % 2 0: del nums[i]运行到一半就报IndexError: list index out of range。原因是每删一个元素列表长度就变了但 range 是预先算好的索引超出当前列表长度自然报错。就算不报错结果也可能不对。看这个nums [2, 4, 5] for num in nums: if num % 2 0: nums.remove(num) print(nums) # [4, 5]根本没删干净为什么会漏掉 4因为遍历的时候当删除第一个 2 后4 自动补到了索引 0 的位置而循环的下一步去检查索引 1也就是原来的 54 就这样被跳过了。6.2 正确的删除姿势高级一点的写法是用列表推导式nums [1, 2, 3, 4, 5, 6] nums [num for num in nums if num % 2 ! 0]这种写法最简洁而且不会出错。如果需要在原列表上修改可以倒序遍历for i in range(len(nums) - 1, -1, -1): if nums[i] % 2 0: del nums[i]倒序删除的原理是删除当前位置的元素只会影响后面元素的位置而你已经处理完后面了所以不受影响。我推荐新手养成用列表推导式过滤列表的习惯既快又安全。7. 浅拷贝和深拷贝改一个全变7.1 赋值不是复制这个坑在写嵌套列表或嵌套字典的时候特别明显matrix [[0, 0, 0]] * 3 print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] matrix[0][0] 1 print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]你有没有发现只改了一个位置三行都变了。因为[[0, 0, 0]] * 3不是复制了三个列表而是复制了同一个列表对象的三个引用。这三个引用指向同一块内存改一处全变。手动写嵌套列表时一定要用列表推导式matrix [[0] * 3 for _ in range(3)]7.2 copy 和 deepcopy 怎么选普通的一维列表拷贝用copy.copy或者切片[:]就够了因为里面元素本身是标量。但如果列表里嵌套了列表或字典浅拷贝只是复制了外层结构内层还是共享的import copy a [[1, 2], [3, 4]] b copy.copy(a) b[0].append(99) print(a) # [[1, 2, 99], [3, 4]]a 也被改了如果内层结构也要独立必须用copy.deepcopyb copy.deepcopy(a)注意deepcopy 在数据量大的时候会慢一些因为它要递归复制所有嵌套对象。如果只是拷贝一层就不要用 deepcopy用 copy 或切片就行。判断标准很简单数据结构里还有没有可变对象没有就浅拷贝嵌套了就要深拷贝。8. 异常处理裸 except 把 bug 全吞了8.1 捕获所有异常结果连报错都看不到新手为了提高代码稳定性喜欢这样写try: result 10 / num except: pass这种做法极其危险。一方面它会吞掉所有异常包括键盘中断、内存错误让你在调试时什么都看不到另一方面异常发生了你不知道程序继续往下走带着错误的数据执行后面的逻辑产生的错误结果比直接崩溃危害更大。正确姿势是捕获具体的异常类型try: result 10 / num except ZeroDivisionError: print(除数不能为0) except TypeError: print(类型错误请检查输入)8.2 把异常信息打出来如果确实需要对多种异常统一处理也要把异常信息保留下来try: result 10 / num except Exception as e: print(f发生异常: {e})特别是写爬虫、写文件处理、写网络请求的时候异常信息是你判断问题在哪的唯一线索。我见过太多代码里裸except: pass出了问题完全不知道怎么排查只能靠猜。保留异常信息的习惯从一开始就要养成。顺便提一句finally和else子句也是新手容易忽略的。finally无论是否异常都会执行适合放关闭文件、断开连接的代码else在没有任何异常时执行适合放正常流程的代码。9. 字符串拼接的效率和编码问题9.1 循环里用 拼字符串越拼越慢新手在循环里拼接字符串是很自然的操作result for item in items: result str(item) ,在小数量级下没感觉但如果 items 有几万条这个循环会非常慢。原因是字符串是不可变对象每次都会创建一个新的字符串对象把旧字符串完整复制一遍再追加内容时间复杂度是 O(n²)。正确的做法是把内容放列表里最后用join一次性拼接parts [] for item in items: parts.append(str(item)) result ,.join(parts)这段代码的时间复杂度是 O(n)效率高得多。如果你要拼的字符串包含变量用 f-string 是更优雅的方案result .join(f{item}, for item in items)9.2 编码问题报 UnicodeEncodeError 怎么办字符串编码问题在读写文件、爬虫抓取网页时特别常见。最常见的是把字符串写到终端或文件时遇到UnicodeEncodeError。解决方案是读写文件时明确指定编码。with open(output.txt, w, encodingutf-8) as f: f.write(content)Python 3 的字符串是 Unicode 对象本身没有编码问题问题出在字节和字符之间的转换。记住这个原则读文件时用 encoding 参数告诉 Python 文件是什么编码写文件时也用 encoding 告诉它用什么编码输出就不会出问题。提醒Windows 系统下默认编码是 GBK如果文件里没有指定 encoding 就写入中文会报UnicodeEncodeError: gbk codec cant encode character。统一指定 utf-8 是最省心的做法。10. 误用可变对象作为字典的 key 或集合元素10.1 列表不能当 key原因不是语法不让有新手写过这样的代码dict_key {[1, 2]: value}直接报TypeError: unhashable type: list。很多人只知道列表不能当 key但不理解为什么。字典的 key 要支持 hash 操作也就是说 key 必须是一个可以通过哈希函数算出固定值、并且不可变的对象。列表是可变对象内容一变哈希值就变了字典无法维护对应关系。如果你需要用一个序列当 key用元组。但如果这个元组内部还嵌套了列表一样不行比如(1, [2, 3])。要保证元组内部所有元素都是不可变的。10.2 集合里的元素也不能用列表同理set里的元素也必须可哈希所以不能把列表放进去。如果你需要去重一个包含多个元素的组合可以把它们转成元组再放入集合combos set() combos.add((1, 2)) combos.add((1, 2)) # 不会重复这个陷阱在数据清洗、去重场景中很常见。比如你对一批点去重点的坐标来自列表你就需要先把列表转成元组才能放进集合或作为字典键。我知道有一个非常经典的需求按列表内容去重并统计数量。如果你拿列表直接当字典的键一运行就报错很多人在这里卡住。正确处理方式如下data [[1, 2], [1, 2], [3, 4]] counter {} for item in data: key tuple(item) counter[key] counter.get(key, 0) 1附常见错误速查表症状 | 原因 | 解决办法 ----------------------------------------|------------------------------|--------------------------------------- IndentationError | 缩进不一致Tab和空格混用 | 统一4空格开启显示空白字符 UnboundLocalError | 函数内赋值被当作局部变量 | 用global/nonlocal或返回值 is 判断值相等但结果不对 | 混淆is和 | 值比较用None判断用is IndexError: list index out of range | 循环中删除导致索引越界 | 用列表推导式过滤或倒序遍历删除 明明只改一个值多个地方一起变了 | 浅拷贝共享内存 | 用copy.deepcopy复制嵌套结构 except: pass 看不到任何错误信息 | 裸捕获吞异常 | 捕获具体异常并打印信息 UnicodeEncodeError | 文件读写未指定编码 | 读写时统一指定 encodingutf-8 列表不能当字典key | 列表不可哈希 | 用元组替代列表作为key 默认参数被多次调用污染 | 可变对象作为默认参数 | 用None占位函数内初始化 整数除法和浮点除法混淆 | 不清楚 / 和 // 的区别 | 明确用 / 做浮点除// 做整数除在实际写代码的过程中我个人的体会是这 10 个坑里可变默认参数、浅拷贝、循环内删除列表是出现频率最高的三个而且它们有一个共同特点——表面不报错结果却和预期不一样。这种静默错误比直接报错难查得多因为它不会给你任何提示只能靠你对 Python 对象模型的理解去排查。所以建议你在学语法的时候多花点时间理解对象的引用与复制、可变与不可变这两个概念把根上弄明白很多坑自然就避开了。最后再分享一个小技巧遇到奇怪的 bug先别急着加 print 猜测先在 IDE 里开启调试模式打断点逐行看变量的内存地址和值的变化。特别是涉及列表、字典这类可变对象的时候把 id() 打出来看一眼你就知道到底是不是同一个对象了。这个习惯一旦养成你排查 bug 的速度会快很多。
返回列表