语法错误与异常
Python 程序中的问题可分为两类:语法错误在代码执行前就被解析器发现,异常则在代码运行期间触发。理解二者的区别是调试的第一步。
语法错误(SyntaxError)
语法错误又称解析错误,是代码不符合 Python 语法规则导致的。这类错误在程序启动时就会被拦截,根本不会进入执行阶段。
>>> while True print('hello')
File "<stdin>", line 1
while True print('hello')
^^^^^
SyntaxError: invalid syntax
解析器会打印出错的行,并用 ^ 标记检测到错误的位置。注意箭头指向的位置不一定是实际要修复的地方——上例中错误在 print() 处被检测到,真正的原因是前面缺少冒号。语法错误无法通过 try/except 捕获,因为它发生在代码编译阶段,而非运行时。
常见的语法错误触发场景包括:括号不匹配、缩进错误(IndentationError,它是 SyntaxError 的子类)、保留字误用、赋值号与等于号混淆等。
# 括号不匹配
print("hello" # SyntaxError: unexpected EOF while parsing
# 缩进错误
def foo():
print("no indent") # IndentationError
# 保留字误用
class = 5 # SyntaxError: invalid syntax
异常(Exception)
即使语法完全正确,语句执行时仍可能出错。这种运行时检测到的错误称为异常。异常不一定会导致程序崩溃——它们可以被捕获和处理。
>>> 10 * (1 / 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam * 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
异常信息由三部分组成:
- Traceback 堆栈回溯:以
Traceback (most recent call last):开头,列出异常发生时的调用链,从最早进入的函数到最终触发异常的代码行。 - 异常类型:如
ZeroDivisionError、NameError、TypeError。标准异常类型是内置标识符,不是保留关键字。 - 异常详情:类型名后面的字符串,描述具体错误原因,如
division by zero。
常见内置异常
Python 内置了丰富的异常体系,覆盖绝大多数运行时错误场景:
| 异常 | 触发场景 | 示例 |
|---|---|---|
ZeroDivisionError | 除数为零 | 1 / 0 |
NameError | 引用未定义的变量 | print(x) 而 x 未赋值 |
TypeError | 操作或函数应用于不适当类型的对象 | len(5)、'2' + 2 |
ValueError | 类型正确但值不合法 | int("abc") |
IndexError | 序列索引越界 | [1,2][5] |
KeyError | 字典中找不到键 | d['missing'] |
AttributeError | 对象没有该属性 | [].missing |
FileNotFoundError | 文件不存在 | open('no.txt') |
OSError | 系统级操作失败 | 文件、网络、权限错误 |
RuntimeError | 不属于其他类别的错误 | 通用运行时错误 |
RecursionError | 递归过深 | 无限递归 |
MemoryError | 内存耗尽 | 创建超大对象 |
KeyboardInterrupt | 用户按 Ctrl+C | 中断程序 |
SystemExit | sys.exit() 被调用 | 主动退出程序 |
# 类型错误:操作数类型不匹配
try:
result = '年龄:' + 25
except TypeError as e:
print(f"类型错误:{e}")
# 修正:result = '年龄:' + str(25)
# 值错误:类型对但值不合法
try:
age = int("不是数字")
except ValueError as e:
print(f"值错误:{e}")
# 索引错误
try:
data = [1, 2, 3]
print(data[10])
except IndexError as e:
print(f"索引越界:{e}")
# 键错误
try:
config = {"host": "localhost"}
print(config["port"])
except KeyError as e:
print(f"缺少配置项:{e}")
异常层次结构
所有异常都继承自 BaseException,但日常编程中更关注它的子类 Exception。Exception 是所有非致命异常的基类,而 SystemExit、KeyboardInterrupt 等直接继承 BaseException 的异常通常表示程序应当终止,一般不被捕获。
BaseException
├── SystemExit # sys.exit() 触发,正常退出
├── KeyboardInterrupt # Ctrl+C 触发
├── GeneratorExit # 生成器关闭时触发
└── Exception # 所有常规异常的基类
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── TypeError
├── ValueError
├── AttributeError
└── OSError
├── FileNotFoundError
├── PermissionError
└── IsADirectoryError
这个层次结构决定了 except 子句的匹配规则:捕获父类时,也会捕获其所有子类。例如 except OSError 会同时捕获 FileNotFoundError 和 PermissionError。
# 利用层次结构进行分级处理
try:
with open("data.txt") as f:
content = f.read()
number = int(content)
result = 100 / number
except FileNotFoundError:
print("文件不存在,使用默认配置")
except PermissionError:
print("权限不足,无法读取文件")
except OSError as e:
print(f"其他系统错误:{e}") # 捕获除上述两种外的 OSError 子类
except ValueError:
print("文件内容不是有效数字")
except ZeroDivisionError:
print("文件中数字为零,无法做除法")
阅读 Traceback
Traceback 是定位问题的关键线索。阅读时应从底部向上看:最下面一行是异常类型和消息,往上是发生异常的代码位置,再往上是调用该代码的函数,依此类推。
def read_config(path):
with open(path) as f:
return int(f.read().strip())
def calculate_rate(config_path):
total = read_config(config_path)
return 1000 / total
def main():
rate = calculate_rate("config.txt")
print(f"汇率:{rate}")
main()
如果 config.txt 内容为 0,Traceback 如下:
Traceback (most recent call last):
File "demo.py", line 12, in <module>
main()
File "demo.py", line 10, in main
rate = calculate_rate("config.txt")
File "demo.py", line 7, in calculate_rate
return 1000 / total
ZeroDivisionError: division by zero
阅读顺序:
- 最底部:
ZeroDivisionError: division by zero—— 知道是除零错误。 File "demo.py", line 7—— 错误发生在calculate_rate函数的第 7 行return 1000 / total。- 再往上 ——
calculate_rate是被main()调用的,main()又在模块顶层被调用。
这样能快速定位到问题根源:total 的值为 0,而 total 来自 read_config 读取的文件内容。
异常参数
异常被触发时可以携带参数,即异常对象关联的值。参数类型和数量取决于异常类型。通过 as 绑定可以访问异常实例,其 .args 属性以元组形式保存所有参数。
try:
raise ValueError("年龄不能为负数", "age", -5)
except ValueError as e:
print(type(e)) # <class 'ValueError'>
print(e.args) # ('年龄不能为负数', 'age', -5)
print(e) # 年龄不能为负数
msg, field, val = e.args
print(f"字段 {field} 的值 {val} 不合法:{msg}")
内置异常类型通常定义了 __str__() 方法,因此直接打印异常实例会显示第一个参数,无需手动访问 .args。自定义异常可以覆盖这一行为,提供更友好的错误信息。