本文面向已有前端开发基础、正在学习 Python 的开发者。
用前端开发中熟悉的 try...catch、throw、错误对象和接口错误处理,理解 Python 的异常处理。
异常处理的目的不是把错误藏起来,而是在程序运行到"不符合预期"的地方时,明确决定下一步怎么做。
如果你有 JavaScript / TypeScript 经验,可以先用这张表建立直觉:
| Python 知识点 | 前端脑内映射 | 先记住 |
|---|---|---|
try |
try |
包住可能出错的代码 |
except |
catch |
出错后进入这里处理 |
except ValueError |
catch 里判断错误类型 |
只处理某一种错误 |
as e |
catch (error) |
拿到错误对象 |
else |
JS 没有完全对应语法 | try 没出错时执行 |
finally |
finally |
不管成功失败都执行 |
raise |
throw |
主动抛出错误 |
Exception |
Error |
常见异常的通用父类 |
| 自定义异常 | class XxxError extends Error |
让错误类型更有业务含义 |
lua
Python 异常处理
发现错误
|
+-- Python 自动抛出异常
+-- 自己用 raise 主动抛出异常
|
捕获错误
|
+-- try / except
+-- except XxxError as e
|
处理结果
|
+-- else 成功后执行
+-- finally 最后一定执行
|
工程表达
|
+-- 自定义异常
+-- raise ... from e
+-- traceback / 日志
先记一句话:
异常 = 程序运行过程中,某一步无法继续正常执行
一、先建立总地图
学习阶段先熟悉这些常见异常:
| 异常类型 | 前端脑内映射 | 常见原因 |
|---|---|---|
NameError |
变量没定义 | 使用了不存在的变量名 |
TypeError |
类型错误 | 类型不支持当前操作 |
ValueError |
值不合法 | 值的格式不对 |
KeyError |
读对象上不存在的 key | 字典里没有这个 key |
IndexError |
数组下标越界 | 列表下标越界 |
AttributeError |
对 null / 错误对象取属性 |
对象没有这个属性或方法 |
ZeroDivisionError |
除法边界错误 | 除数是 0 |
从前端角度看,JS 里很多运行时问题最后都表现成 TypeError 或普通 Error。Python 会把错误分得更细,比如字典 key 不存在是 KeyError,列表下标越界是 IndexError。
简单演示。下面每一行都是独立示例,不是让它们一次性全部运行:
python
name # NameError,变量不存在
10 + '1' # TypeError,类型不支持当前操作
int('abc') # ValueError,值的格式不对
user = {}
user['name'] # KeyError,字典里没有这个 key
items = []
items[0] # IndexError,列表下标越界
value = None
value.strip() # AttributeError,对象没有这个方法
10 / 0 # ZeroDivisionError,除数是 0
二、try except
1. 前端脑内映射
Python 的 try...except 对应 JS 的 try...catch。
把可能出错的代码放进 try,把出错后的处理放进 except。
python
try:
age = int('abc')
except ValueError:
print('年龄必须是数字')
对应 JS 里的写法大概是:
js
try {
const age = Number.parseInt('abc', 10);
if (Number.isNaN(age)) {
throw new Error('年龄必须是数字');
}
} catch (error) {
console.log(error.message);
}
差异在于:Python 的 int('abc') 会直接抛出 ValueError,而 JS 的 Number.parseInt('abc', 10) 会得到 NaN,你通常要自己判断。
2. 执行顺序
执行顺序:
rust
try 里的代码正常
-> 继续往下执行
try 里的某一行出错
-> 后面的 try 代码不再执行
-> 跳到匹配的 except
多个异常可以分别处理:
python
data = {'count': 'abc'}
try:
count = int(data['count'])
except KeyError:
print('缺少 count 字段')
except ValueError:
print('count 必须是数字')
这点和 JS 最大的区别是:Python 可以按异常类型分多个 except;JS 通常是在一个 catch 里自己判断错误类型。
3. 多个异常合并处理
多个异常处理逻辑一样,可以写成元组:
python
try:
count = int(data['count'])
except (KeyError, ValueError):
print('count 参数不正确')
不要随手写空 except:
python
try:
count = int(data['count'])
except:
print('出错了')
这种写法会捕获太多错误,容易把真正的问题藏起来。
4. Exception 兜底放最后
如果需要兜底,写 Exception,并且放在最后:
python
try:
count = int(data['count'])
except KeyError:
print('缺少 count')
except ValueError:
print('count 必须是数字')
except Exception as e:
print(f'未知异常:{e}')
多个 except 会从上往下匹配。越具体的异常越应该写在前面,越宽泛的异常越应该写在后面。
三、获取异常信息
1. 前端脑内映射
JS 里常见写法是 catch (error),Python 里对应的是 except XxxError as e。
js
try {
throw new Error('年龄必须是数字');
} catch (error) {
console.log(error.message);
console.log(error.name);
}
2. Python 写法
用 as e 可以拿到异常对象。
python
try:
age = int('abc')
except ValueError as e:
print(e) # 错误提示
print(type(e).__name__) # 异常类型名称
print(e.args) # 异常参数
常用信息:
| 写法 | 作用 |
|---|---|
str(e) |
错误提示文本 |
type(e).__name__ |
异常类型名称 |
e.args |
异常参数 |
如果想看完整调用栈,用标准库 traceback:
python
import traceback
try:
age = int('abc')
except ValueError:
print(traceback.format_exc())
traceback 可以理解成 Python 里的 stack trace,能看到错误发生在哪个文件、哪一行、经过了哪些函数调用。
学习阶段可以用 print。真实项目里通常用日志记录,不要只留一句"出错了"。
四、else、finally 和 raise
1. try / except / else / finally
完整结构是:
rust
try
-> 尝试执行
except
-> 出错时执行
else
-> 没有出错时执行
finally
-> 无论是否出错,最后都执行
前端熟悉的 try...catch...finally 在 Python 里多了一个常用分支:else。
vbnet
JS: try / catch / finally
Python: try / except / else / finally
例子:
python
try:
age = int('18')
except ValueError:
print('年龄必须是数字')
else:
print(f'转换成功:{age}')
finally:
print('处理结束')
finally 常用于释放资源,比如关闭文件、关闭数据库连接、释放锁。
2. raise 主动抛出异常
有些错误不是 Python 自动发现的,而是你根据规则判断出来的,这时用 raise 主动抛出异常。
python
def parse_age(value):
age = int(value)
if age < 0:
raise ValueError('年龄不能小于 0')
return age
对应 JS:
js
function parseAge(value) {
const age = Number(value);
if (age < 0) {
throw new Error('年龄不能小于 0');
}
return age;
}
调用方再决定怎么处理:
python
try:
age = parse_age('-1')
except ValueError as e:
print(f'参数错误:{e}')
函数内部负责发现问题并 raise,函数外部再用 try except 决定怎么处理。
五、自定义异常和异常链
1. 自定义异常
内置异常能表达通用问题,比如 ValueError 表示值不合法。
如果你想表达更具体的业务含义,可以自定义异常。
前端里如果要区分不同错误,可能会写:
ts
class UserNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'UserNotFoundError';
}
}
Python 里通常这样写:
python
class UserError(Exception):
"""用户相关异常的基类。"""
class UserNotFoundError(UserError):
"""用户不存在。"""
使用:
python
def find_user(user_id):
users = {1: 'Tom', 2: 'Jerry'}
if user_id not in users:
raise UserNotFoundError(f'用户不存在:{user_id}')
return users[user_id]
捕获:
python
try:
name = find_user(3)
except UserNotFoundError as e:
print(e)
自定义异常的好处是:异常类型本身就能表达含义。
rust
ValueError
-> 只能知道"值不对"
UserNotFoundError
-> 一眼知道"用户不存在"
如果一组异常属于同一类,可以定义共同父类:
python
class AppError(Exception):
"""项目异常基类。"""
class ConfigError(AppError):
"""配置错误。"""
class AppPermissionError(AppError):
"""权限错误。"""
这样可以统一捕获:
python
try:
raise ConfigError('缺少数据库配置')
except AppError as e:
print(f'应用异常:{e}')
2. raise from 保留异常链
有时底层错误比较细,直接暴露给上层不够清楚。可以用 raise ... from e 转换异常,同时保留原始原因。
python
class AgeError(Exception):
"""年龄参数错误。"""
def parse_age(value):
try:
return int(value)
except ValueError as e:
raise AgeError('年龄必须是数字') from e
含义:
arduino
表层原因:AgeError,年龄必须是数字
底层原因:ValueError,int 转换失败
这样既能给上层一个清楚的异常类型,又不会丢掉原始错误。
六、实践原则
异常处理最容易出问题的地方,不是语法,而是捕获范围太大、处理太随意。
从前端经验迁移过来,可以这样理解:
arduino
不要到处 catch 后什么都不做
不要把所有错误都变成一个"系统异常"
不要给用户暴露完整 stack trace
不要把开发排查需要的信息丢掉
- 不要随手写空
except: - 不要把所有代码都包进一个巨大的
try - 能捕获具体异常,就不要一开始捕获
Exception Exception兜底要放在最后- 捕获异常后要处理、记录,或者继续抛出,不要悄悄吞掉
- 自定义异常通常继承
Exception - 函数内部可以只负责
raise,外层再统一try except - 给用户看的提示要简洁,给开发看的错误信息要完整