前言

程序出错是常态,文件处理是日常。这两块写优雅了,脚本的质量直接上一个档次。

一、异常基础

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 篇。