前言
程序出错是常态,文件处理是日常。这两块写优雅了,脚本的质量直接上一个档次。
一、异常基础
try:
result = 10 / int(user_input)
except ValueError as e: # 精确捕获, 别裸 except!
print(f"输入不是数字: {e}")
except ZeroDivisionError:
print("除数不能为零")
else:
print(f"结果: {result}") # 无异常才执行(放成功逻辑, try 块保持精简)
finally:
print("总是执行") # 清理逻辑(关连接等)
异常层次结构(理解它就不会乱捕获):
BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
└── Exception # ← 日常异常的共同祖先
├── ValueError
├── TypeError
├── KeyError / IndexError
├── FileNotFoundError / PermissionError (OSError 子类)
├── ConnectionError / TimeoutError (OSError 子类)
└── ...
二、反模式与正确姿势
# ❌ 反模式1: 裸 except 吞掉一切(连 Ctrl+C 都吞)
try:
main()
except:
pass
# ❌ 反模式2: 捕获过宽
try:
parse(cfg)
except Exception:
...
# ✅ 姿势: 捕获你预期的、能处理的
try:
parse(cfg)
except (ValueError, KeyError) as e:
logger.error("配置格式错误: %s", e)
raise # 处理不了就继续往上抛
raise 的三种用法:
raise # 重新抛出当前异常(保留现场)
raise RuntimeError("bad state") from e # 异常链: 换类型但保留根因
raise RuntimeError(f"port {p} 非法") # 抛新异常
三、自定义异常
class AppError(Exception):
"""应用基础异常"""
class ConfigError(AppError):
def __init__(self, file: str, reason: str):
super().__init__(f"config {file}: {reason}")
self.file, self.reason = file, reason
def load_config(path):
if not os.path.exists(path):
raise ConfigError(path, "文件不存在")
# 调用方可以按层次精确处理
try:
load_config("/etc/myapp/app.conf")
except ConfigError as e:
print(e, e.file) # 额外属性携带上下文
经验法则:写库/框架时定义异常基类;写脚本时 Exception 信息带足上下文即可。
四、文件操作:with 是唯一正确姿势
# ❌ 老写法: 忘了 close 或中途异常泄漏句柄
f = open("data.txt")
data = f.read()
f.close()
# ✅ with: 无论是否异常都自动关闭
with open("data.txt", encoding="utf-8") as f:
data = f.read()
常用模式:
# 读: 一次 vs 逐行(大文件必逐行!)
with open("access.log", encoding="utf-8") as f:
content = f.read() # 小文件
with open("access.log", encoding="utf-8") as f:
for line in f: # 流式读取, 内存友好
if "ERROR" in line:
...
# 写: 覆盖 / 追加
with open("out.txt", "w", encoding="utf-8") as f:
f.write("hello\n")
with open("out.txt", "a") as f:
f.write("more\n")
# 编码永远显式声明! Windows 默认 GBK, 跨机器就乱码
五、pathlib:路径操作现代化
from pathlib import Path
p = Path("/var/log/myapp/app.log")
p.name # 'app.log'
p.stem # 'app'
p.suffix # '.log'
p.parent # Path('/var/log/myapp')
p.exists(), p.is_file(), p.is_dir()
# 拼接(用 / 运算符, 跨平台)
conf = Path("/etc") / "myapp" / "app.conf"
# 遍历
for f in Path("/var/log").glob("*.log"):
print(f)
# 建目录 / 读写文本(pathlib 自带快捷方法!)
Path("/opt/app/data").mkdir(parents=True, exist_ok=True)
cfg = conf.read_text(encoding="utf-8")
conf.write_text("debug=false\n", encoding="utf-8")
# 安全删除
import shutil
shutil.rmtree("/tmp/build") # 删目录树
六、结构化数据:JSON 与 CSV
import json
from pathlib import Path
# 读写 JSON
conf = {"host": "0.0.0.0", "port": 8080, "debug": False}
Path("conf.json").write_text(
json.dumps(conf, indent=2, ensure_ascii=False), # 中文不转义
encoding="utf-8",
)
loaded = json.loads(Path("conf.json").read_text(encoding="utf-8"))
import csv
# 读写 CSV
with open("hosts.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f)) # [{name:..., ip:...}, ...]
with open("out.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["name", "ip"])
w.writeheader()
w.writerows([{"name": "web01", "ip": "10.0.1.11"}])
七、实战:健壮的配置加载器
综合运用本篇知识点:
import json
from pathlib import Path
class ConfigError(Exception):
pass
def load_config(path: str) -> dict:
p = Path(path)
try:
raw = p.read_text(encoding="utf-8")
except FileNotFoundError:
raise ConfigError(f"配置文件不存在: {p}") from None
except PermissionError:
raise ConfigError(f"无权读取: {p}") from None
try:
conf = json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError(f"JSON 语法错误 (行{e.lineno}): {e.msg}") from None
# 必填字段校验
for key in ("host", "port"):
if key not in conf:
raise ConfigError(f"缺少必填字段: {key}")
return conf
if __name__ == "__main__":
import logging
try:
print(load_config("/etc/myapp/app.json"))
except ConfigError as e:
logging.error("启动失败: %s", e)
raise SystemExit(1)
小结
| 场景 | 姿势 |
|---|---|
| 捕获 | 精确类型 + as e,处理不了就 raise |
| 清理 | finally 或更好的 with |
| 文件 | 永远 with open(..., encoding="utf-8") |
| 路径 | pathlib.Path + / 拼接 |
| 大文件 | 逐行迭代,别 read() 全量 |
| 自定义异常 | 继承 Exception,信息带上下文 |
本文是「Python」系列第 5 篇。