错误和异常
程序运行过程中难免会遇到各种错误。Python 把错误分成两大类:语法错误和异常。理解它们的区别,并学会用异常处理机制写出更健壮的代码,是进阶 Python 编程的重要一步。
1. 语法错误
语法错误(Syntax Error) 是代码还没开始运行就被解释器发现的错误,也叫解析错误。初学者最常遇到,通常是因为漏写了冒号、括号不匹配、缩进错误等。
while True print('Hello world')运行后会输出类似下面的信息:
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax解释器会指出出错的大致位置,并用 ^ 标记最先发现错误的地方。上面的例子中,print() 前面少了一个冒号 :,导致 while 语句语法不完整。
2. 异常
即使语法完全正确,程序运行期间也可能发生错误,这种运行期检测到的错误被称为异常(Exception)。
# 0 不能作为除数
10 * (1 / 0)
# 使用了未定义的变量
4 + spam * 3
# 字符串和整数不能直接相加
'2' + 2对应报错:
ZeroDivisionError: division by zero
NameError: name 'spam' is not defined
TypeError: can only concatenate str (not "int") to str异常信息包含两部分:
- 异常类型:如
ZeroDivisionError、NameError、TypeError。 - 错误描述:说明具体出错原因。
3. 异常处理
3.1 try/except 基本用法
使用 try/except 语句可以捕获异常,防止程序因意外错误而直接崩溃。
while True:
try:
x = int(input('请输入一个数字: '))
break
except ValueError:
print('您输入的不是数字,请再次尝试输入!')执行逻辑:
- 先执行
try块中的代码。 - 如果没有异常发生,跳过
except块。 - 如果发生异常,且异常类型与
except后面的名称匹配,执行对应的except块。 - 如果没有匹配的
except块,异常会继续向上层抛出。
异常处理不仅能捕获
try子句中直接发生的异常,还能捕获try子句中调用的函数(甚至间接调用的函数)里抛出的异常。
def this_fails():
x = 1 / 0
try:
this_fails()
except ZeroDivisionError as err:
print(f'处理运行期错误: {err}')3.2 捕获多个异常
一个 try 可以跟多个 except,分别处理不同类型的异常。也可以把多种异常写成一个元组。
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print(f'OS error: {err}')
except ValueError:
print('无法把数据转换成整数。')
except:
print(f'未知错误: {sys.exc_info()[0]}')
raise# 同时捕获多种异常
except (RuntimeError, TypeError, NameError):
pass最后一个不带异常类型的 except 相当于通配符,但尽量少用,避免隐藏真正的 bug。
4. try/except...else
try 还可以带一个 else 子句,只有当 try 块没有抛出异常时才会执行。else 必须放在所有 except 之后。
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except IOError:
print(f'无法打开 {arg}')
else:
print(f'{arg} 有 {len(f.readlines())} 行')
f.close()把「没有异常时才执行」的代码放到 else 中,比全部塞进 try 块更清晰,也能避免捕获到意料之外的异常。
5. try-finally
finally 子句中的代码无论是否发生异常都会执行,常用于释放资源、关闭文件、断开连接等清理操作。
try/except/else/finally 四个子句的作用可以概括如下:
| 子句 | 执行时机 |
|---|---|
try | 执行可能出错的代码 |
except | 发生异常时执行的代码 |
else | 没有异常时执行的代码 |
finally | 不管有没有异常都会执行的代码 |
try:
raise KeyboardInterrupt
finally:
print('Goodbye, world!')输出:
Goodbye, world!
KeyboardInterrupt即使在 try 中发生了未被捕获的异常,finally 也会先执行完,再把异常抛出。
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print('除数不能为 0')
else:
print(f'结果是 {result}')
finally:
print('执行 finally 子句')
divide(2, 1) # 正常情况
divide(2, 0) # 捕获异常
divide('2', '1') # 未捕获的 TypeError,finally 执行后仍会抛出同一个 try 语句里也可以同时包含 except、else 和 finally:
try:
do_something()
except AssertionError as error:
print(error)
else:
try:
with open('file.log') as file:
read_data = file.read()
except FileNotFoundError as fnf_error:
print(fnf_error)
finally:
print('这句话,无论异常是否发生都会执行。')6. 抛出异常
使用 raise 语句可以主动抛出异常。
x = 10
if x > 5:
raise Exception(f'x 不能大于 5。x 的值为: {x}')输出:
Exception: x 不能大于 5。x 的值为: 10raise 的参数可以是异常类,也可以是异常实例。只想把捕获到的异常继续向上抛时,可以直接用 raise:
try:
raise NameError('HiThere')
except NameError:
print('一个异常飞过去了!')
raise7. 用户自定义异常
通过继承 Exception 类,可以创建自己的异常类型。
class MyError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
try:
raise MyError(2 * 2)
except MyError as e:
print(f'我的异常发生了,value: {e.value}')如果一个模块可能抛出多种相关异常,通常会先定义一个基础异常类,再派生具体的异常子类:
class Error(Exception):
"""本模块的异常基类。"""
pass
class InputError(Error):
"""输入错误。"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
class TransitionError(Error):
"""状态转换错误。"""
def __init__(self, previous, next_state, message):
self.previous = previous
self.next_state = next_state
self.message = message按照惯例,自定义异常类名通常以 Error 结尾。
8. 预定义的清理行为
一些对象自带标准的清理行为,最常见的就是文件对象。使用 with 语句可以确保对象使用完毕后自动调用清理方法,即使中间发生异常也不例外。
# 不推荐:发生异常时文件可能没关闭
for line in open('myfile.txt'):
print(line, end='')# 推荐:with 语句自动关闭文件
with open('myfile.txt') as f:
for line in f:
print(line, end='')with 语句本质上是上下文管理器,它让资源管理更简洁、更安全。
9. 常见内置异常速查
| 异常类型 | 说明 |
|---|---|
SyntaxError | 语法错误 |
ZeroDivisionError | 除数为 0 |
NameError | 使用了未定义的变量 |
TypeError | 类型不匹配 |
ValueError | 值不合法 |
KeyError | 字典中找不到指定键 |
IndexError | 索引越界 |
AttributeError | 对象没有该属性 |
FileNotFoundError | 文件不存在 |
OSError | 操作系统错误 |
10. 综合案例
下面是一个简单的数字输入处理函数,演示了 try/except/else/finally 的组合用法:
def get_int(prompt):
try:
value = int(input(prompt))
except ValueError:
print('输入错误:必须是整数。')
return None
else:
print(f'你输入的是 {value}')
return value
finally:
print('输入处理完成')
num = get_int('请输入一个整数: ')
if num is not None:
print(f'输入有效: {num}')11. 调试
程序出问题时,找到 bug 的过程就是调试。Python 提供了几种常用的调试手段。
11.1 print 调试
最简单直接的方式:在关键位置打印变量值。
def add(a, b):
print(f'调试: a={a}, b={b}')
return a + b11.2 断言 assert
assert 用于判断某个条件是否成立,不成立时抛出 AssertionError。
def divide(a, b):
assert b != 0, '除数不能为 0'
return a / b
# divide(1, 0) # AssertionError: 除数不能为 011.3 日志 logging
相比 print,logging 模块更适合记录程序运行信息,可以设置级别、输出到文件。
import logging
logging.basicConfig(level=logging.INFO)
logging.info('程序启动')
logging.warning('这是一条警告')logging 有五个级别,从低到高是 DEBUG、INFO、WARNING、ERROR、CRITICAL。basicConfig(level=...) 设置了最低输出级别,低于它的日志不会显示:
import logging
logging.basicConfig(level=logging.INFO)
# 不会输出,级别低于 INFO
logging.debug('debug 级别的日志')
# 会输出
logging.info('info 级别的日志')
# 会输出
logging.warning('warning 级别的日志')在 except 里用 logging.exception() 可以记录完整的错误调用栈,方便排查问题:
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
# 打印完整的错误调用栈
logging.exception(e)
main()
# 输出:
# ERROR:root:division by zero
# Traceback (most recent call last):
# ...
# ZeroDivisionError: division by zero错误发生时会沿着函数的调用链一层层向外抛,直到被 try 捕获,这个过程记录下来的就是调用栈(Traceback)。logging.exception() 会把调用栈完整打印出来,相比 print 更规范,也可以配置输出到文件而不是控制台。
总结
Python 的错误分为语法错误和异常。异常处理的核心是 try/except/else/finally:
try放可能出错的代码。except捕获并处理特定异常。else在无异常时执行。finally无论是否异常都执行,适合资源清理。
需要主动报告错误时用 raise;需要自定义语义更明确的异常时,可以继承 Exception。定位问题时可以用 print、assert、logging 或 IDE 调试器进行调试。对于文件、网络连接等资源,优先使用 with 语句,让清理行为自动发生。