【Python全栈开发】第5讲 | 异常处理、文件 IO 与项目实战:构建你的第一个生产级工具
1. 墨菲定律:如果代码可能出错,它就一定会出错
兄弟们,如果你写的程序只在"理想情况"下运行,那它充其量只是个玩具。
真正的生产级代码,80% 的精力都在处理那些"不理想"的情况:
- 数据库连不上了怎么办?
- 用户输入的不是数字怎么办?
- 想要读取的文件被别人删了怎么办?
今天,咱们就来学习怎么给你的程序拉起一张"安全网"(异常处理),以及怎么让你的程序拥有"记忆"(文件操作)。
2. 异常处理:别让你的程序动不动就崩
2.1 异常处理机制原理
在深入代码之前,我们先理解 Python 异常处理的底层机制。
什么是异常?
异常是程序执行过程中发生的错误事件,会打断正常的指令流。Python 使用异常对象来表示这些错误,当错误发生时,会创建一个异常对象并抛出(raise)。
异常处理的工作流程:
1. 程序正常执行
2. 发生错误 -> 创建异常对象
3. 抛出异常 -> 中断当前执行流
4. 沿调用栈向上传播
5. 被 try-except 捕获处理,或导致程序崩溃
Python 异常层次结构:
BaseException (所有异常的基类)
├── SystemExit # 解释器退出
├── KeyboardInterrupt # 用户按下 Ctrl+C
├── GeneratorExit # 生成器关闭
└── Exception # 常规异常的基类
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── TypeError
├── ValueError
├── FileNotFoundError
└── ...
捕获异常的本质:
当异常被抛出时,Python 会沿着调用栈向上查找匹配的 except 子句。这就像抛接球游戏——异常被抛出,直到有人接住(处理)它。
2.2 try-except 的基本姿势
try:
num = int(input("请输入一个数字:"))
result = 10 / num
print(f"结果是:{result}")
except ValueError:
print("兄弟,让你输数字,别瞎填!")
except ZeroDivisionError:
print("数学老师没教过你吗?0 不能做除数!")
2.3 完整的防御体系
除了 try 和 except,还有两个好兄弟:
else:如果代码没出事,就执行这里。finally:不管出不出事,最后都得执行这里(比如关闭数据库连接)。
try:
f = open("data.txt", "r")
except FileNotFoundError:
print("文件找不到了...")
else:
print("读取成功,正在处理数据...")
finally:
print("清理现场,任务结束。")
2.4 自定义异常类设计
在实际项目中,我们经常需要定义自己的异常类型,让错误信息更加语义化。
为什么需要自定义异常?
- 提供更具体的错误信息
- 区分业务逻辑错误和系统错误
- 让代码更具可读性和可维护性
设计自定义异常的规范:
# 基础:继承 Exception 类
class ValidationError(Exception):
"""数据验证失败时抛出"""
pass
# 进阶:添加额外信息
class BusinessError(Exception):
"""业务逻辑错误"""
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code
self.message = message
# 实战:完整的异常体系
class AppException(Exception):
"""应用基础异常"""
def __init__(self, message, error_code="UNKNOWN"):
super().__init__(message)
self.error_code = error_code
self.message = message
class ConfigError(AppException):
"""配置相关错误"""
def __init__(self, message, config_key=None):
super().__init__(message, error_code="CONFIG_ERROR")
self.config_key = config_key
class FileOperationError(AppException):
"""文件操作错误"""
def __init__(self, message, file_path=None):
super().__init__(message, error_code="FILE_ERROR")
self.file_path = file_path
# 使用示例
def load_config(path):
if not path.endswith('.json'):
raise ConfigError("配置文件必须是 JSON 格式", config_key="file_type")
# ... 其他逻辑
def read_data(file_path):
try:
with open(file_path, 'r') as f:
return f.read()
except FileNotFoundError as e:
raise FileOperationError(f"文件不存在: {file_path}", file_path=file_path) from e
异常链(Exception Chaining):
使用 raise ... from ... 可以保留原始异常信息,方便调试。
try:
risky_operation()
except ValueError as e:
raise BusinessError("业务处理失败") from e
2.5 避坑指南:禁止使用裸 except
我见过很多新手为了省事,写出 except: pass。这简直是犯罪!它会吞掉所有的错误信息,让你在 Debug 时想撞墙。永远要明确指出你要捕获什么异常。
错误示范:
try:
do_something()
except: # [警告] 捕获所有异常,包括 KeyboardInterrupt!
pass
正确做法:
try:
do_something()
except SpecificError as e: # 明确指定异常类型
handle_error(e)
3. 文件操作:让你的数据"活"下去
3.1 文件描述符和操作系统交互
什么是文件描述符?
文件描述符(File Descriptor)是操作系统内核用于访问文件的一个抽象指示符。在 Python 中,当你调用 open() 时:
- Python 向操作系统请求打开文件
- 操作系统分配一个文件描述符(一个整数)
- 操作系统在进程的文件描述符表中记录该文件信息
- Python 返回一个文件对象,内部持有这个描述符
# 查看文件描述符
f = open("test.txt", "w")
print(f.fileno()) # 输出类似 3、4、5 的数字,这就是文件描述符
f.close()
操作系统层面的文件操作:
- 打开文件:内核分配文件描述符,检查权限
- 读写文件:通过描述符定位磁盘数据,进行 IO 操作
- 关闭文件:释放文件描述符,刷新缓冲区到磁盘
为什么必须关闭文件?
- 资源限制:每个进程能打开的文件数量是有限的(通常是 1024 或更多)
- 数据完整性:写操作通常先进入缓冲区,关闭时才会刷到磁盘
- 数据一致性:其他进程可能正在等待访问该文件
import os
# 查看系统文件描述符限制
print(f"软限制: {os.getrlimit(os.RLIMIT_NOFILE)}")
# 不关闭文件会导致资源泄漏
def leak_demo():
for i in range(10000):
f = open("test.txt", "w") # 不关闭!
# 最终会因 "Too many open files" 报错
3.2 编码与字符集深度解析
字符集基础:
- ASCII:128 个字符,1 字节表示,只能表示英文
- GBK/GB2312:中文编码,1-2 字节,Windows 中文系统默认
- UTF-8:Unicode 的可变长编码,1-4 字节,国际通用
编码问题的本质:
编码是将字符映射为二进制数据,解码是反向过程。当编码和解码使用不同标准时,就会出现乱码。
字符 '中' 的不同编码:
- UTF-8: 0xE4 0xB8 0xAD (3字节)
- GBK: 0xD6 0xD0 (2字节)
- Unicode: 0x4E2D (码点)
Python 中的编码处理:
# 字符串在内存中是 Unicode
text = "Hello 世界"
# 编码:str -> bytes
utf8_bytes = text.encode('utf-8')
gbk_bytes = text.encode('gbk')
print(f"UTF-8: {utf8_bytes}") # b'Hello \xe4\xb8\x96\xe7\x95\x8c'
print(f"GBK: {gbk_bytes}") # b'Hello \xc0\xfb\xbd\xe7'
# 解码:bytes -> str
decoded = utf8_bytes.decode('utf-8')
print(decoded) # Hello 世界
# 错误处理
try:
gbk_bytes.decode('utf-8') # 用 UTF-8 解码 GBK 数据
except UnicodeDecodeError as e:
print(f"解码失败: {e}")
# 安全解码(忽略错误字符)
safe_text = gbk_bytes.decode('utf-8', errors='ignore')
print(safe_text) # Hello
文件编码的最佳实践:
# 始终显式指定编码
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 处理未知编码的文件
import chardet
with open("unknown.txt", "rb") as f:
raw_data = f.read()
result = chardet.detect(raw_data)
print(f"检测到编码: {result['encoding']}, 置信度: {result['confidence']}")
text = raw_data.decode(result['encoding'])
3.3 别再手动 close() 了
以前我们写文件要先 open 再 close,万一中间崩了,文件就没关上,数据可能就丢了。
现在统一用 with 语句(上下文管理器):
# 写入文件
with open("note.txt", "w", encoding="utf-8") as f:
f.write("Python 全栈开发,从零到精通!\n")
f.write("坚持就是胜利。")
# 读取文件
with open("note.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
老司机建议:永远带上 encoding="utf-8",否则在 Windows 上你会遇到让你崩溃的乱码问题。
3.4 上下文管理器(Context Manager)实现原理
什么是上下文管理器?
上下文管理器是实现了 __enter__ 和 __exit__ 方法的对象,用于管理资源的获取和释放。
工作原理:
class MyContext:
def __enter__(self):
print("进入上下文 - 获取资源")
return self # 返回的对象会赋值给 as 后的变量
def __exit__(self, exc_type, exc_val, exc_tb):
print("退出上下文 - 释放资源")
# exc_type: 异常类型
# exc_val: 异常值
# exc_tb: 异常追踪信息
# 返回 True 会抑制异常,False 会让异常继续传播
return False
# 使用
with MyContext() as ctx:
print("执行业务逻辑")
使用 contextlib 简化实现:
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"获取资源: {name}")
resource = {"name": name, "data": []}
try:
yield resource # 这里返回给 with 语句
finally:
print(f"释放资源: {name}")
# 使用
with managed_resource("数据库连接") as conn:
conn["data"].append("查询结果")
print(f"使用资源: {conn}")
# 自定义文件上下文管理器
@contextmanager
def safe_open(file_path, mode='r', encoding='utf-8'):
"""安全的文件打开上下文管理器"""
f = None
try:
f = open(file_path, mode, encoding=encoding)
yield f
except FileNotFoundError:
print(f"文件不存在: {file_path}")
raise
except PermissionError:
print(f"权限不足,无法访问: {file_path}")
raise
finally:
if f:
f.close()
print(f"文件已关闭: {file_path}")
# 使用
with safe_open("test.txt", "w") as f:
f.write("Hello, World!")
3.5 现代化的路径管理:pathlib
别再用 os.path.join 那种又长又丑的写法了。pathlib 才是现代 Python 的标准:
from pathlib import Path
# 获取当前目录下名为 'data' 的文件夹里的 'test.json'
data_file = Path.cwd() / "data" / "test.json"
if data_file.exists():
print(f"找到文件:{data_file}")
else:
print("文件不存在,正在创建目录...")
data_file.parent.mkdir(parents=True, exist_ok=True)
4. JSON:程序间的"通用语言"
在全栈开发里,Python 经常要和前端(JavaScript)或者其他服务打交道。JSON 是最常用的数据交换格式。
import json
user_info = {
"name": "张三",
"is_admin": True,
"tags": ["Python", "后端", "全栈"]
}
# 序列化:把 Python 字典转成字符串存起来
json_str = json.dumps(user_info, ensure_ascii=False)
with open("user.json", "w", encoding="utf-8") as f:
f.write(json_str)
# 反序列化:把字符串读回来变回字典
with open("user.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"读取到的用户:{data['name']}")
5. 综合实战:简易配置管理器
咱们写一个能自动读写 JSON 配置文件的类,还要带上健壮的异常处理。
import json
from pathlib import Path
class ConfigError(Exception):
"""配置相关错误"""
pass
class ConfigManager:
def __init__(self, file_path):
self.file_path = Path(file_path)
self.config = {}
self.load()
def load(self):
"""从文件加载配置"""
try:
if self.file_path.exists():
with open(self.file_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
else:
print(f"提示:配置文件 {self.file_path} 不存在,将使用默认配置。")
except json.JSONDecodeError as e:
raise ConfigError(f"配置文件格式错误: {e}")
except PermissionError:
raise ConfigError(f"无权限读取配置文件: {self.file_path}")
except Exception as e:
raise ConfigError(f"加载配置失败: {e}")
def save(self):
"""保存当前配置到文件"""
try:
# 确保目录存在
self.file_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=4, ensure_ascii=False)
print("配置已保存。")
except PermissionError:
raise ConfigError(f"无权限写入配置文件: {self.file_path}")
except Exception as e:
raise ConfigError(f"保存配置失败: {e}")
def get(self, key, default=None):
return self.config.get(key, default)
def set(self, key, value):
self.config[key] = value
self.save()
# 使用示例
try:
cfg = ConfigManager("settings.json")
print(f"当前主题:{cfg.get('theme', 'default')}")
cfg.set("theme", "dark")
except ConfigError as e:
print(f"配置错误: {e}")
6. 避坑小贴士(老司机的叮嘱)
- 别把异常处理当成逻辑判断:能用
if解决的(比如判断文件是否存在),尽量别用try-except。异常捕获是有性能开销的。 - 文件路径坑:Windows 用反斜杠
\, Mac/Linux 用正斜杠/。养成用pathlib或者os.path.join的习惯,别手动拼路径,否则你的代码一换系统就崩。 - 资源泄露:除了文件,数据库连接、网络 Socket 等也都要记得用
with或者在finally里关闭。
7. 实战演练:巩固你的内功
题目 1:简易日志记录器
需求:
编写一个函数 log_message(msg, level="INFO")。
它将日志写入 app.log 文件,格式为:[2023-10-27 10:00:00] [INFO] 你的消息内容。
提示:使用 datetime 模块获取时间,使用 a 模式追加写入。
import datetime
def log_message(msg, level="INFO"):
# 获取当前时间并格式化
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{now}] [{level}] {msg}\n"
try:
# 使用 'a' 模式追加,不会覆盖旧日志
with open("app.log", "a", encoding="utf-8") as f:
f.write(log_entry)
print("日志写入成功。")
except PermissionError:
print("错误:无权限写入日志文件")
except Exception as e:
print(f"日志写入失败:{e}")
# 测试
log_message("系统启动成功")
log_message("数据库连接超时", "ERROR")
题目 2:批量重命名文件
需求:
假设当前目录下有一个 images 文件夹(你需要先手动创建它并放几个文件进去,比如 a.txt, b.jpg)。
编写一个脚本,把 images 文件夹里所有的文件重命名为 file_1.txt, file_2.jpg 这种格式(保留原后缀名)。
提示:使用 pathlib 模块遍历和重命名。
from pathlib import Path
def batch_rename(folder_path):
folder = Path(folder_path)
if not folder.exists():
print("文件夹不存在!")
return
# 获取所有文件(不包含子文件夹)
files = [f for f in folder.iterdir() if f.is_file()]
for index, file in enumerate(files):
# file.suffix 获取后缀名(如 .txt)
new_name = f"file_{index + 1}{file.suffix}"
new_path = folder / new_name
try:
file.rename(new_path)
print(f"重命名:{file.name} -> {new_name}")
except FileExistsError:
print(f"重命名失败:{new_name} 已存在")
except PermissionError:
print(f"重命名 {file.name} 失败:权限不足")
except Exception as e:
print(f"重命名 {file.name} 失败:{e}")
# 测试前请确保当前目录下有 images 文件夹
# batch_rename("images")
题目 3:安全除法计算器
需求:
编写一个循环,让用户输入两个数字 a 和 b,计算 a / b。
- 如果输入不是数字,提示"请输入数字"。
- 如果
b是 0,提示"除数不能为 0"。 - 如果计算成功,打印结果并退出循环。
- 无论是否成功,都打印"本次计算结束"。
while True:
try:
a = float(input("请输入被除数:"))
b = float(input("请输入除数:"))
result = a / b
except ValueError:
print("错误:请输入纯数字!")
except ZeroDivisionError:
print("错误:除数不能为 0!")
else:
print(f"结果是:{result:.2f}")
break # 成功了就退出
finally:
print("--- 本次计算结束 ---")
8. 系列索引
写在最后:
这一讲学完,你已经具备了编写"稳健程序"的能力。你的程序不再是玻璃心,而是拥有了对抗错误和持久化数据的能力。
建议你试着给之前的学生管理系统加上文件保存功能:每次运行程序先读取students.json,退出前自动保存。
觉得有收获的话,点赞、收藏,咱们下一讲聊聊怎么让你的程序跑得更快——并发编程!
更多推荐


所有评论(0)