错误是 Python 告诉你发生了什么的方式
每个 Python 错误都是一个对象,它有类型、消息和 traceback,也就是导致这个错误的一连串调用。能读懂错误,是你能学到的最重要的调试技能。本页带你认识实际会遇到的错误,以及能让你快速修好它们的习惯。
想深入了解 try/except 的原理以及如何抛出自己的异常,异常一页讲解了相关语法。本页讲的是具体的错误,以及怎么读懂它们。
读懂 traceback
运行一段会出错的代码:
def divide(a, b):
return a / b
def report(values):
for v in values:
print(divide(10, v))
report([5, 2, 0])
Python 会打印类似这样的内容:
Traceback (most recent call last):
File "script.py", line 8, in <module>
report([5, 2, 0])
File "script.py", line 6, in report
print(divide(10, v))
File "script.py", line 2, in divide
return a / b
ZeroDivisionError: division by zero
从下往上读:
ZeroDivisionError: division by zero:异常类型和消息。这告诉你出了“什么”错。divide中第 2 行的return a / b:真正抛出异常的那一行。report中第 6 行的print(divide(10, v)):触发它的那次调用。- 模块级别第 8 行的
report([5, 2, 0]):一切开始的地方。
最底部的那一帧几乎总是要修改的地方。当某个库在它内部深处抛出错误时,沿着 traceback 往上找,找到第一个属于“你自己”代码的帧,那就是你传入了错误输入的那次调用。
最常遇到的错误
NameError
“name 'mesage' is not defined”。原因是拼写错误,或者在变量赋值之前就使用了它。在较新的版本中,Python 的错误信息通常会给出一个可能的正确名字(“Did you mean 'message'?”)。
修复方法:检查拼写和作用域。如果这个名字只存在于某个函数内部,你就不能在函数外部读取它。
TypeError
“can only concatenate str (not "int") to str”。操作用的类型不对。典型情况:把字符串和数字相加,调用一个不可调用的东西,给函数传了错误数量的参数。
修复方法:显式转换类型(str(30)、int("30")),或者检查你实际传入的是什么。跨类型拼接时,f-string 通常比用 + 拼接更好读:f"age: {30}"。
ValueError
“invalid literal for int() with base 10: 'hello'”。类型是对的,int() 接受字符串,但这个“值”不合适。常见于 int()、float()、日期时间解析,以及接收有范围限制的参数的函数。
修复方法:在转换前先验证,或者捕获错误并处理它:
KeyError
“KeyError: 'charlie'”。这个键不在字典里。有三种惯用的修复方法,根据意图来选:
如果你希望字典在键不存在时自动初始化,collections.defaultdict 值得一看。
IndexError
“list index out of range”。你要取的位置在序列中并不存在。
修复方法:先检查长度,用 -1 取最后一个元素,或者使用切片(numbers[5:6] 返回 [],而不会抛出异常)。
AttributeError
“'NoneType' object has no attribute 'upper'”。你在一个没有某个方法的东西上调用了这个方法。这几乎总是意味着某个变量的类型出乎意料,经常是你以为有值,它却是 None。
修复方法:找出这个 None 是从哪里来的。print(type(var)) 或者在出错前一行设个断点,是最快的方法。“有时会失败”的函数通常会返回 None;在它的返回值上调用方法之前,先检查一下返回值。
ModuleNotFoundError(以及 ImportError)
import fastapi
“No module named 'fastapi'”。这个包没有安装在你的脚本所使用的 Python 解释器中。两个常见原因:
- 你确实还没安装它。在正确的环境中运行
python -m pip install fastapi。 - 你安装了,但装进了另一个 Python。在 macOS 上,
pip和python指向不同的安装时,这种情况经常发生。
可靠的修复方法是,用你运行代码的同一个解释器来安装:
python -m pip install fastapi
如果你使用虚拟环境(你应该使用),确保在运行 pip install 和 python script.py 之前都已经激活了它。
FileNotFoundError
with open("settings.yaml") as f:
config = f.read()
“[Errno 2] No such file or directory: 'settings.yaml'”。相对于脚本运行的位置,这个路径并不存在。
修复方法:在脚本开头打印 os.getcwd(),确认 Python 是在哪里查找的。使用绝对路径,或者用 pathlib.Path(__file__).parent / "settings.yaml" 把路径固定到脚本自身所在的位置。
EOFError
name = input("Name: ")
“EOF when reading a line”。input() 尝试从标准输入读取,却什么也没读到:要么输入是从一个空的来源通过管道传进来的,要么你在提示符下按了 Ctrl-D。
修复方法:如果通过管道传入是正常的使用方式,就把这次调用包起来:
try:
name = input("Name: ")
except EOFError:
name = "anonymous"
在这些文档页面的浏览器编辑器中,运行环境会模拟输入,所以你在那里不会遇到这个错误。
IndentationError 和 SyntaxError
IndentationError: expected an indented block after function definition on line 2
这两个错误很特殊:它们在你的程序运行之前就会触发。Python 拒绝解析这个文件。
IndentationError:def、if、for等语句的代码体缺失或没有对齐。大多数编辑器都能把缩进显示出来;打开“显示空白字符”,就能发现混用的制表符和空格。SyntaxError:你忘了冒号,括号没有配对,或者关键字拼错了。较新的 Python 版本会用一个箭头(^)指向出问题的字符。
修复方法:错误信息会指出是哪一行。去看看。如果那一行看不出什么问题,就检查它“上面”的行:几行之前一个没闭合的括号,常常会在很久之后才表现为语法错误。
RuntimeError
这是一个兜底的错误,表示“运行时出了问题,但不属于那些更具体的错误”。RecursionError(超过了递归深度)是它常见的一个子类。库代码处于异常状态时,经常会抛出 RuntimeError。
修复方法:读一下消息,它通常描述得很清楚。如果原因是递归,要么改写成迭代循环,要么在少数情况下调高 sys.setrecursionlimit。
用 print 调试
在动用真正的调试器之前,print(),或者更好的 f"{var=}" 写法,就能抓住大多数 bug:
f"{var=}" 会同时打印表达式的源码和它的值,所以你不用在格式字符串里再打一遍名字。运行脚本,扫一眼输出,找到某个值开始出错的那一行。大多数“神秘”的 bug,在放上三四个位置恰当的 print 之后就会一目了然。
提交代码前要把这些 print 清理掉。对于要发布的代码,logging.debug(...) 比 print 更好:你不用修改代码就能打开或关闭调试日志。
breakpoint() 和 pdb
当 print 不够用时,breakpoint() 会在那个位置把你带进 Python 的交互式调试器:
def discount(price, percent):
breakpoint()
return price * (1 - percent / 100)
discount(100, 20)
在真实的终端中运行这个脚本。你会停在一个 (Pdb) 提示符处。几个需要知道的命令:
p variable:打印一个变量。n:执行到下一行。s:进入函数调用内部。c:继续运行,直到下一个断点或程序结束。q:退出。l:列出当前行附近的源码。
IDE 调试器(VS Code、PyCharm)用图形界面包装了同样的协议:在行号旁边设置断点,侧边栏显示变量。哪个用起来更顺手就选哪个。
减少出错的好习惯
- 使用有描述性的变量名。 当你不可能忘记变量里装的是什么时,一半的
TypeError和AttributeError就不会出现了。 - 在边界处验证输入。 在函数开头一次性解析和检查用户输入或文件内容。这样其余代码就可以信任这些值。
- 大声地失败,而不是悄无声息。 一个光秃秃的
except Exception: pass会把你真正需要看到的错误藏起来。捕获具体的异常,有意识地处理它们,其余的让它们继续向上传递。 - 先读 traceback 的最底部。 花在学习读 traceback 上的每一分钟,都会给你带来百倍的回报。
大多数错误都不是什么谜团:它们是一行拼写错误或类型用错,而且错误信息写得很清楚。相信错误信息,它通常是对的。
你做到了
参考部分到这里就结束了。你从“Python 是什么?”出发,学习了变量、控制流、集合、函数、类、迭代、真实世界中的数据以及错误。接下来的步骤要以项目为导向:挑一个你想做的东西,倒推出需要深入学习的知识点。当你带着具体的问题回来时,这些页面都还在这里。
常见问题
Python 中的 KeyError 是什么?
当你查找字典中不存在的键时,就会抛出 KeyError。users["missing"] 会抛出 KeyError: 'missing'。安全的替代写法有 users.get("missing", default)、users.get("missing")(返回 None),或者显式地用 if key in users: 先判断。
Python 中的 EOFError 是什么?
当 input() 因为输入流已关闭而读不到任何内容时,就会抛出 EOFError(EOF 即文件结束)。最常见的情况是,一个使用 input() 的脚本通过管道运行却没有传入数据,或者你在交互式提示符下按了 Ctrl-D。如果你的脚本必须处理管道输入,就用 try/except EOFError 来防护。
ModuleNotFoundError 是什么?
ModuleNotFoundError 表示 import X 没有找到名为 X 的模块。要么是这个包没有安装(pip install X),要么是它装在了另一个 Python 里,而不是运行你代码的那个 Python(装了多个 Python 时非常常见)。python -m pip install X 会使用同一个解释器,能解决第二种情况。
怎么读 Python 的 traceback?
从下往上读。最后一行是异常类型和消息,也就是具体出了什么错。上面的几行显示了导致错误的调用栈,你自己的代码通常更靠近底部。点击或跳转到最底部属于你自己代码的那一帧所在的文件和行号,那里就是要修改的地方。