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 完整的防御体系

除了 tryexcept,还有两个好兄弟:

  • 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() 时:

  1. Python 向操作系统请求打开文件
  2. 操作系统分配一个文件描述符(一个整数)
  3. 操作系统在进程的文件描述符表中记录该文件信息
  4. Python 返回一个文件对象,内部持有这个描述符
# 查看文件描述符
f = open("test.txt", "w")
print(f.fileno())  # 输出类似 3、4、5 的数字,这就是文件描述符
f.close()

操作系统层面的文件操作:

  • 打开文件:内核分配文件描述符,检查权限
  • 读写文件:通过描述符定位磁盘数据,进行 IO 操作
  • 关闭文件:释放文件描述符,刷新缓冲区到磁盘

为什么必须关闭文件?

  1. 资源限制:每个进程能打开的文件数量是有限的(通常是 1024 或更多)
  2. 数据完整性:写操作通常先进入缓冲区,关闭时才会刷到磁盘
  3. 数据一致性:其他进程可能正在等待访问该文件
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()

以前我们写文件要先 openclose,万一中间崩了,文件就没关上,数据可能就丢了。
现在统一用 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. 避坑小贴士(老司机的叮嘱)

  1. 别把异常处理当成逻辑判断:能用 if 解决的(比如判断文件是否存在),尽量别用 try-except。异常捕获是有性能开销的。
  2. 文件路径坑:Windows 用反斜杠 \, Mac/Linux 用正斜杠 /。养成用 pathlib 或者 os.path.join 的习惯,别手动拼路径,否则你的代码一换系统就崩。
  3. 资源泄露:除了文件,数据库连接、网络 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:安全除法计算器

需求
编写一个循环,让用户输入两个数字 ab,计算 a / b

  1. 如果输入不是数字,提示"请输入数字"。
  2. 如果 b 是 0,提示"除数不能为 0"。
  3. 如果计算成功,打印结果并退出循环。
  4. 无论是否成功,都打印"本次计算结束"。
点击查看参考答案
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,退出前自动保存。
觉得有收获的话,点赞、收藏,咱们下一讲聊聊怎么让你的程序跑得更快——并发编程!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐