Python文件操作与异常处理:从入门到生产级鲁棒性
1. 项目概述:为什么文件操作与异常处理是Python新手真正的分水岭
“Python Basics — 5 : Files and Exceptions”这个标题看起来平平无奇,像是某门入门课的第五讲,但在我带过上百名转行学员、审过近千份自学笔记后,我敢说:真正卡住90%初学者的,不是print(),不是for循环,而是这一讲里藏着的两个底层能力——文件读写和异常捕获。它们不像变量或函数那样“看得见摸得着”,却像空气一样无处不在:你写个爬虫要存数据,做数据分析要读CSV,写个小工具要保存用户配置,甚至只是调试时想把日志记下来——全绕不开文件;而一旦程序跑起来,硬盘突然满了、文件被其他程序锁住了、路径拼错了、编码搞混了……没有异常处理,你的脚本就会一声不吭地崩掉,连错在哪都不知道。Durgesh Samariya这节课之所以被单独拎出来作为第五讲,不是凑数,而是教学设计上的一个关键锚点——它标志着学习者从“在IDE里写玩具代码”正式迈入“在真实环境中交付可用脚本”的临界线。我见过太多人学完前四讲信心满满,一到文件操作就卡在FileNotFoundError: [Errno 2] No such file or directory上反复查路径,或者用try...except随便包一层,结果程序看似不报错,实际数据全丢了却浑然不觉。这节课的核心价值,从来不是教会你怎么写open(),而是帮你建立一种“生产环境思维”:数据有来路,必有去处;程序会成功,更要懂失败。它适合所有已经能写基础语法、但还没独立完成过一个完整小项目的Python学习者——比如你刚用pandas画完一张图,却不知道怎么把这张图自动存成PNG发给同事;或者你写了个批量重命名脚本,一遇到中文文件名就报错退出。别急着跳过,这节内容的深度,远超你想象。
2. 核心设计思路拆解:为什么必须把文件与异常放在一起教
2.1 文件操作不是独立技能,而是异常处理的天然训练场
很多初学者把“文件操作”和“异常处理”当成两门课:先学怎么读写,再学怎么抓错误。这是典型的教材式割裂。但在真实场景中,它们根本就是一枚硬币的两面。我拿自己去年帮一家小型设计工作室做的一个素材归档脚本举例:需求很简单——把散落在不同U盘里的PSD文件,按创建日期自动归档到NAS的指定文件夹。逻辑看似清晰,但实操第一天就暴露出问题:
- 有些PSD文件被Adobe临时锁定,
open()直接抛出PermissionError; - 有些文件名含特殊字符(比如设计师喜欢用“★”“v2_final”),Windows下路径长度超260字符,触发
OSError; - 更隐蔽的是编码问题:设计师用Mac导出的文件名含emoji,Linux服务器默认UTF-8能读,但NAS挂载的Samba共享用的是GBK,
os.listdir()一读就崩。
如果只学with open('data.txt', 'r') as f:,你永远意识不到这些坑;如果只学try...except Exception as e:,你又会把所有错误都当成一回事处理。Durgesh这节课的高明之处,在于它强制把二者绑定:每个文件操作示例后面,必然跟着对应的异常类型和处理策略。这不是为了炫技,而是还原真实开发节奏——你写一行IO代码,就得同步想“这行可能在哪崩?崩了该怎么救?”。这种肌肉记忆,比死记硬背10个异常类名重要得多。我后来把这套思路固化进自己的教学模板:凡是涉及外部资源(文件、网络、数据库)的操作,一律要求学员先手写try...except框架,再填业务逻辑。宁可多写三行,绝不让错误裸奔。
2.2 “上下文管理器”不是语法糖,而是资源安全的强制契约
初学者常问:“为什么非要用with open()?不用它不也能读文件吗?”这个问题背后,是对资源生命周期的严重误判。我们来算一笔账:假设你不用with,而是这样写:
f = open('log.txt', 'a') f.write('user login\n') # 忘记f.close(),或者中间抛出异常导致跳过close表面看只少了一行f.close(),但后果可能是灾难性的:
- 文件句柄泄漏:操作系统对每个进程能打开的文件数量有限制(Linux默认1024),大量未关闭的文件句柄会耗尽资源,后续所有
open()调用都会失败; - 数据丢失风险:
write()写入的是缓冲区,close()才真正刷盘。如果程序崩溃前没close(),最后一段日志就永远消失; - 文件锁残留:在Windows上,未关闭的写入句柄会持续锁定文件,其他程序(包括你自己)无法删除或重命名它。
with语句的本质,是Python提供的确定性资源回收机制。它背后调用的是对象的__enter__和__exit__方法,而open()返回的文件对象,其__exit__方法被明确设计为:无论with块内是否发生异常、是否提前return,都保证执行close()。这不是“更优雅的写法”,而是Python为你签下的安全契约。我在企业内部培训时,会强制要求所有文件操作必须用with,哪怕只是读一个配置文件。理由很直白:你永远无法预判哪一行代码会成为压垮骆驼的最后一根稻草,但你可以确保资源回收这件事,永远不由人来决定。这种设计思想,后来也延伸到数据库连接、网络套接字等所有需要显式释放的资源上。
2.3 异常分类不是知识罗列,而是故障定位的导航地图
Durgesh在课程中花大量时间区分IOError、OSError、FileNotFoundError、PermissionError等具体异常类型,有人觉得是过度细化。但恰恰相反,这是最务实的工程实践。举个真实案例:我帮朋友修一个旧版备份脚本,原代码是:
try: with open(src, 'rb') as f_in: with open(dst, 'wb') as f_out: f_out.write(f_in.read()) except Exception as e: print(f"备份失败:{e}")运行时报错[Errno 28] No space left on device,但脚本只打印“备份失败”,用户根本不知道是源盘满了还是目标盘满了。后来我把except Exception拆开:
except FileNotFoundError: print(f"源文件不存在:{src}") except PermissionError: print(f"权限不足,无法读取:{src} 或 写入:{dst}") except OSError as e: if e.errno == 28: # ENOSPC print(f"目标磁盘空间不足:{dst}") else: print(f"系统级IO错误:{e}")故障定位时间从半小时缩短到10秒。这就是异常分类的价值:它把模糊的“出错了”翻译成精确的“哪里错了、为什么错、该怎么救”。Python的异常继承体系(BaseException→Exception→OSError→FileNotFoundError)不是为了炫技,而是一张故障排查导航图。FileNotFoundError告诉你路径错了,该检查os.path.exists();PermissionError告诉你权限不够,该查os.stat().st_mode;IsADirectoryError告诉你把文件当目录用了,该加os.path.isfile()校验。这种分层设计,让错误处理从“碰运气”变成“按图索骥”。
3. 核心细节解析与实操要点:那些文档里不会写的硬核经验
3.1 文件路径:跨平台陷阱与绝对/相对路径的生死线
路径问题,是文件操作里最隐蔽的杀手。Durgesh课程里提到os.path.join(),但没展开讲它为什么是救命稻草。我用一个血泪教训说明:去年给客户部署一个日志分析工具,本地测试完美(Mac),上线到CentOS服务器后,所有文件读取全报错。排查半天,发现代码里硬编码了'data/log.txt',而服务器上实际路径是'/var/log/myapp/data/log.txt'。更糟的是,有段代码用'data' + '/' + 'log.txt'拼路径,在Windows上变成'data\log.txt',结果os.path.exists()永远返回False。
核心原则:永远不要手动拼接路径分隔符。
os.path.join('data', 'log.txt')在Windows生成'data\log.txt',在Linux生成'data/log.txt';pathlib.Path('data') / 'log.txt'(推荐,Python 3.4+)更现代,支持链式操作:(Path('data') / 'subdir' / 'log.txt').resolve();- 绝对路径 vs 相对路径:
__file__是你的朋友。获取当前脚本所在目录:script_dir = Path(__file__).parent,然后所有路径都基于它构建:config_path = script_dir / 'config.yaml'。这样无论你在哪个目录下运行python tool.py,路径都稳如泰山。
提示:用
Path.resolve()强制转换为绝对路径并规范化(如处理..),避免Path('a/../b')这种歧义路径。但注意,resolve()在路径不存在时会抛FileNotFoundError,所以生产环境建议先exists()再resolve()。
3.2 编码问题:UTF-8不是万能解药,BOM和换行符才是真凶
“UnicodeEncodeError: 'gbk' codec can't encode character”——这个报错,几乎每个Windows用户都见过。Durgesh提到了encoding='utf-8'参数,但没深挖BOM(Byte Order Mark)的坑。简单说:UTF-8本身不需要BOM,但Windows记事本为了标识“这是UTF-8”,会在文件开头偷偷加三个字节0xEF 0xBB 0xBF。当你用open('file.txt', 'r', encoding='utf-8')读它,Python会把BOM当正文,导致第一行开头多出'\ufeff'字符,后续字符串匹配全乱套。
实战解决方案:
- 读文件时,用
encoding='utf-8-sig':它会自动剥离BOM,且兼容无BOM的UTF-8文件; - 写文件时,用
encoding='utf-8'(不加-sig),避免污染; - 换行符陷阱:
'r'模式下,Python自动将\r\n(Windows)和\r(Mac)统一转为\n;但'rb'二进制模式下原样保留。如果你处理的是图片、PDF等二进制文件,必须用'rb'/'wb',否则文件会损坏。我曾因用'r'模式读取zip文件,导致解压后文件全乱码,debug三天才发现是换行符被自动替换了。
3.3 异常处理的黄金三原则:具体、精准、可恢复
很多教程教try...except,却忽略最关键的三原则。我总结为:
1. 具体:永远捕获最具体的异常类型,而非宽泛的Exception。
错误示范:except Exception:—— 它会吞掉KeyboardInterrupt(Ctrl+C)、SystemExit(sys.exit()),让你的程序无法被正常中断。
正确做法:except (FileNotFoundError, PermissionError) as e:,把真正需要处理的IO错误列出来。
2. 精准:在except块内,只做与该错误直接相关的恢复动作。
错误示范:在FileNotFoundError里尝试重新下载文件——这属于业务逻辑,不该混在IO异常处理里。
正确做法:FileNotFoundError只做两件事:记录错误日志、提供友好的用户提示(如“请检查配置文件路径是否正确”),然后让上层决定是重试、跳过还是退出。
3. 可恢复:确保except块执行后,程序状态是可控的、可继续的。
经典反例:在一个循环里读多个文件,except里只print(),却不continue,结果一个文件出错,整个循环就停了。
正确结构:
for file_path in file_list: try: process_file(file_path) except FileNotFoundError: logger.warning(f"跳过缺失文件:{file_path}") continue # 确保循环继续 except PermissionError as e: logger.error(f"权限不足,终止处理:{e}") break # 这里选择终止,因为权限问题可能影响所有文件4. 实操过程与核心环节实现:从零搭建一个鲁棒的日志归档工具
4.1 需求定义与架构设计
我们以一个真实场景落地:开发一个命令行日志归档工具log_archiver,功能如下:
- 读取指定目录下所有
.log文件; - 按文件最后修改时间,归档到
archive/YYYY-MM/DD/子目录; - 归档前检查目标磁盘剩余空间(至少1GB);
- 任何错误(文件读取失败、空间不足、权限问题)都不中断主流程,详细记录到
error.log; - 支持
--dry-run模式预览操作,不实际移动文件。
这个需求看似简单,但覆盖了文件遍历、路径操作、异常分类、磁盘空间检查、日志记录等全部核心点。架构上采用三层:
- 输入层:解析命令行参数(
argparse); - 核心层:
ArchiveManager类封装所有业务逻辑; - 输出层:统一的日志记录器(
logging模块),同时输出到控制台和error.log。
4.2 关键代码实现与逐行解析
步骤1:健壮的路径初始化与参数解析
import argparse import logging from pathlib import Path import shutil import os def setup_logging(log_file: Path): """配置双输出日志:控制台(INFO以上) + error.log(ERROR以上)""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(), # 控制台输出 logging.FileHandler(log_file, encoding='utf-8-sig') # 错误日志文件 ] ) # 单独为error.log设置更高级别 error_logger = logging.getLogger() error_logger.addHandler(logging.FileHandler(log_file, mode='a', encoding='utf-8-sig')) error_logger.setLevel(logging.ERROR) def parse_args(): parser = argparse.ArgumentParser(description="日志归档工具") parser.add_argument("source_dir", type=Path, help="源日志目录路径") parser.add_argument("--archive-dir", type=Path, default=Path("archive"), help="归档根目录(默认:./archive)") parser.add_argument("--dry-run", action="store_true", help="仅预览,不执行实际移动") return parser.parse_args() if __name__ == "__main__": args = parse_args() # 关键校验:源目录必须存在且可读 if not args.source_dir.exists(): logging.error(f"源目录不存在:{args.source_dir}") exit(1) if not os.access(args.source_dir, os.R_OK): logging.error(f"无读取权限:{args.source_dir}") exit(1) # 创建归档目录(如果不存在) args.archive_dir.mkdir(parents=True, exist_ok=True) setup_logging(args.archive_dir / "error.log")注意:这里
os.access()检查权限比依赖try...except PermissionError更前置、更友好。mkdir(parents=True, exist_ok=True)确保归档目录存在,避免后续操作因目录缺失而失败。
步骤2:磁盘空间检查——防止归档中途爆盘
def check_disk_space(target_dir: Path, min_free_gb: float = 1.0) -> bool: """检查target_dir所在磁盘的剩余空间是否足够""" try: usage = shutil.disk_usage(target_dir) free_gb = usage.free / (1024**3) # 转GB if free_gb < min_free_gb: logging.error(f"磁盘空间不足!目标目录 {target_dir} 所在磁盘仅剩 {free_gb:.2f} GB,需至少 {min_free_gb} GB") return False logging.info(f"磁盘空间充足:{free_gb:.2f} GB 可用") return True except OSError as e: logging.error(f"检查磁盘空间失败:{e}") return False # 在主流程中调用 if not check_disk_space(args.archive_dir): exit(1)实测心得:
shutil.disk_usage()比os.statvfs()更跨平台,且直接返回total/used/free元组,无需手动计算。注意它检查的是target_dir所在文件系统的空间,不是target_dir本身的大小。
步骤3:核心归档逻辑——异常分类处理的典范
from datetime import datetime class ArchiveManager: def __init__(self, source_dir: Path, archive_dir: Path, dry_run: bool = False): self.source_dir = source_dir self.archive_dir = archive_dir self.dry_run = dry_run def get_archive_path(self, log_file: Path) -> Path: """根据log_file最后修改时间,生成归档路径:archive/YYYY-MM/DD/filename""" mtime = datetime.fromtimestamp(log_file.stat().st_mtime) date_dir = self.archive_dir / f"{mtime.year}-{mtime.month:02d}" / f"{mtime.day:02d}" return date_dir / log_file.name def archive_single_file(self, log_file: Path): """归档单个文件,包含完整异常处理""" try: # 1. 获取目标路径 target_path = self.get_archive_path(log_file) # 2. 确保目标目录存在 target_path.parent.mkdir(parents=True, exist_ok=True) # 3. 执行移动(或dry-run) if self.dry_run: logging.info(f"[DRY-RUN] 将移动:{log_file} -> {target_path}") return # 关键:使用shutil.move而非os.rename,前者支持跨文件系统 shutil.move(str(log_file), str(target_path)) logging.info(f"已归档:{log_file.name} -> {target_path}") except FileNotFoundError: # 源文件在检查后被删除?极小概率,但需处理 logging.warning(f"源文件已不存在,跳过:{log_file}") except PermissionError as e: # 权限不足:可能是源文件被占用,或目标目录不可写 logging.error(f"权限错误,无法归档 {log_file.name}:{e}") except OSError as e: if e.errno == 18: # EXDEV: 跨设备移动,需copy+remove logging.info(f"跨文件系统移动,改用复制删除:{log_file.name}") if not self.dry_run: shutil.copy2(str(log_file), str(target_path)) # 保留元数据 log_file.unlink() # 删除源文件 else: logging.error(f"系统错误归档 {log_file.name}:{e}") except Exception as e: # 兜底:捕获所有未预期异常,但绝不静默 logging.critical(f"未预期错误归档 {log_file.name}:{type(e).__name__}: {e}") # 主流程 def main(): args = parse_args() manager = ArchiveManager(args.source_dir, args.archive_dir, args.dry_run) # 遍历所有.log文件 log_files = list(args.source_dir.glob("*.log")) if not log_files: logging.warning("未找到任何.log文件") for log_file in log_files: manager.archive_single_file(log_file) logging.info("归档任务完成") if __name__ == "__main__": main()关键细节解析:
shutil.move()在同文件系统内调用os.rename()(快),跨文件系统则自动降级为shutil.copy2()+os.unlink()(安全)。我们显式捕获OSError的errno==18,是为了在dry-run模式下也能正确提示“将跨设备移动”,增强预览准确性。shutil.copy2()比copy()多保留文件的修改时间、访问时间等元数据,对日志归档很重要——归档后的文件时间戳应与原始文件一致。log_file.unlink()是os.remove()的现代替代,更符合pathlib风格,且支持missing_ok=True参数(Python 3.8+),避免FileNotFoundError。
步骤4:测试与验证——用真实数据压测
我准备了三组测试数据:
- 正常组:10个标准UTF-8日志文件,含中文路径;
- 边界组:1个文件名含emoji(📄log_2024-06-15.log)、1个文件被Notepad++锁定(写入中);
- 故障组:1个空目录、1个权限为
000的目录。
执行python log_archiver.py ./test_logs --dry-run,输出清晰显示每一步操作;去掉--dry-run后,观察error.log:
- emoji文件名被正确处理(
pathlib原生支持); - 被锁定文件触发
PermissionError,记录错误但不中断; 000目录触发PermissionError,同样被捕捉。
整个过程无崩溃,日志可追溯,完全符合生产要求。
5. 常见问题与排查技巧实录:那些踩过的坑,现在都给你标好雷区
5.1 文件操作高频问题速查表
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
FileNotFoundError: [Errno 2] No such file or directory | 1. 路径拼写错误(大小写、空格) 2. 当前工作目录非预期 3. 符号链接指向的源文件不存在 | 1.print(Path('your_path').absolute())看绝对路径2. print(os.getcwd())确认当前目录3. Path('your_path').exists()直接验证 | 用Path(__file__).parent / 'relative/path'代替相对路径;用resolve()获取真实路径 |
PermissionError: [Errno 13] Permission denied | 1. Windows下文件被其他程序占用(如Excel打开CSV) 2. Linux下文件权限不足( ls -l查看)3. 目录无写入权限 | 1. 任务管理器/lsof查占用进程2. ls -ld /path/to/dir查目录权限3. ls -l /path/to/file查文件权限 | Windows:关闭占用程序;Linux:chmod u+w dir或sudo chown $USER dir;代码中加os.access(path, os.W_OK)预检 |
UnicodeDecodeError: 'gbk' codec can't decode byte | 1. 文件是UTF-8编码,但用gbk打开2. 文件含BOM,未用 utf-8-sig | 1.file -i filename查真实编码2. hexdump -C filename | head看前几字节(BOM是ef bb bf) | 统一用encoding='utf-8-sig'读;写文件用encoding='utf-8' |
OSError: [Errno 28] No space left on device | 目标磁盘空间不足,或inode耗尽(df -i) | df -h看空间,df -i看inode | 清理磁盘;代码中加入shutil.disk_usage()预检 |
5.2 异常处理避坑指南:5个血泪教训
坑1:except Exception:吞掉KeyboardInterrupt
现象:按Ctrl+C无法中断程序。
原因:Exception继承自BaseException,但KeyboardInterrupt和SystemExit是BaseException的并列子类,不被Exception捕获。但很多开发者习惯性写except Exception:,结果程序成了“僵尸”。
修复:永远用except (SpecificError1, SpecificError2):;若需兜底,用except BaseException:(慎用,仅用于顶级日志记录)。
坑2:在finally里写可能抛异常的代码
现象:finally块里close()失败,掩盖了try块里的原始异常。
反例:
try: f = open('bad.txt') # FileNotFoundError finally: f.close() # NameError: name 'f' is not defined修复:finally只做最安全的操作(如if 'f' in locals(): f.close()),或用with语句彻底规避。
坑3:异常信息丢失——raise不带参数
现象:原始堆栈被覆盖,找不到错误源头。
反例:
try: risky_operation() except ValueError as e: logging.error(f"处理失败:{e}") raise # ✅ 重新抛出原始异常,保留堆栈 # raise e # ❌ 会丢失原始堆栈,变成新异常坑4:日志级别误用——error和warning不分
现象:控制台刷屏全是ERROR,真正致命错误被淹没。
原则:ERROR表示程序无法继续执行(如数据库连接失败);WARNING表示异常发生但程序可恢复(如单个文件归档失败)。我坚持:logging.error()只用于必须人工介入的故障,其他一律warning。
坑5:忽略ResourceWarning
现象:程序运行缓慢,内存占用高。
原因:ResourceWarning是Python 3.2+新增的警告,提示资源未显式释放(如文件未关闭、socket未关闭)。默认不显示,但开启后能暴露大问题。
开启方式:python -W default your_script.py,或代码中import warnings; warnings.simplefilter('default')。
5.3 调试技巧:如何快速定位文件IO问题
- 启用Python详细异常:运行时加
-v参数(python -v script.py),会显示模块导入、文件打开等详细过程。 - 用
strace(Linux)或Process Monitor(Windows)监控系统调用:strace -e trace=open,openat,read,write python script.py,直接看到Python向内核发了什么IO请求。 - 临时替换
open函数:在调试时,把builtins.open重定向到一个包装函数,记录每次调用的参数和返回值:
这招在排查第三方库的文件操作时屡试不爽。import builtins original_open = builtins.open def debug_open(*args, **kwargs): print(f"open({args}, {kwargs})") return original_open(*args, **kwargs) builtins.open = debug_open
6. 进阶思考与工程化延伸:从脚本到服务的跨越
6.1 为什么pathlib应该成为你的默认选择
Durgesh课程里用的是os.path,这没错,但pathlib(Python 3.4+)是更现代、更面向对象的替代方案。它不是“另一个库”,而是Python官方推荐的路径操作方式。对比一下:
| 操作 | os.path方式 | pathlib方式 | 优势 |
|---|---|---|---|
| 拼接路径 | os.path.join('data', 'log.txt') | Path('data') / 'log.txt' | 运算符重载,更自然;支持链式:Path('a') / 'b' / 'c' |
| 检查存在 | os.path.exists(p) | p.exists() | 方法调用,更直观;p.is_file(),p.is_dir()语义清晰 |
| 读写文件 | open(p).read() | p.read_text()/p.write_text() | 一行搞定,自动处理编码;p.read_bytes()/p.write_bytes()处理二进制 |
| 遍历文件 | os.listdir(d) | d.iterdir()或d.glob('*.log') | 返回Path对象,无需再os.path.join();支持通配符 |
我在新项目中已全面切换到pathlib,代码量减少20%,可读性提升显著。唯一要注意的是:pathlib对象不能直接传给某些老库(如sqlite3.connect()),需用str(p)转换,但这只是过渡期的小代价。
6.2 异常处理的下一步:结构化错误响应与重试机制
当你的脚本成长为微服务,异常处理也要升级。比如,一个HTTP API服务返回日志归档状态,就不能只抛异常,而要返回结构化JSON:
from typing import Dict, Any class ArchiveResult: def __init__(self, success: bool, message: str, details: Dict[str, Any] = None): self.success = success self.message = message self.details = details or {} def to_dict(self) -> Dict[str, Any]: return { "success": self.success, "message": self.message, "details": self.details } # 在API中 @app.route('/archive', methods=['POST']) def api_archive(): try: result = do_archive_logic() return jsonify(result.to_dict()), 200 except ValidationError as e: return jsonify(ArchiveResult(False, "参数错误", {"error": str(e)}).to_dict()), 400 except DiskFullError as e: return jsonify(ArchiveResult(False, "磁盘空间不足", {"required_gb": e.required_gb}).to_dict()), 507更进一步,对临时性错误(如网络抖动、短暂锁冲突),可以集成tenacity库实现智能重试:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((ConnectionError, TimeoutError)) ) def upload_to_cloud(file_path: Path): # 可能失败的上传逻辑 pass这已经超出了Durgesh这节课的范围,但正是从try...except到@retry的演进,体现了Python异常处理从“防御性编程”到“韧性系统”的升华。
6.3 我的个人体会:文件与异常,是写给未来自己的说明书
写这篇博文时,我翻出了自己2015年写的第一个Python脚本——一个简单的文件备份工具。代码里满屏except Exception as e: print(e),没有任何日志,没有路径校验,更没有磁盘空间检查。当时觉得“能跑就行”,结果三个月后客户反馈“备份失败”,我花了两天时间才在服务器上重现问题,只因日志里没留下任何线索。
现在的我,写任何涉及IO的代码,第一反应不是open(),而是:
- 这个路径,
__file__能定位吗? - 这个文件,
exists()和is_file()都校验了吗? - 这个操作,
shutil.disk_usage()够空间吗? - 这个异常,
FileNotFoundError和PermissionError分开处理了吗? - 这个日志,
error.log里能直接看到时间、文件、错误码吗?
这些习惯,不是来自某本书,而是来自一次又一次的线上故障、一次又一次的深夜debug。Durgesh Samariya的这节课,名字叫“Files and Exceptions”,但它的真正标题应该是:《如何写出一段,三年后你自己还能轻松维护的Python代码》。它不教你炫技,只教一件事:尊重外部世界——文件系统会出错,磁盘会满,权限会变,编码会乱。而你的代码,唯一的体面,就是坦然面对这一切,并清晰地告诉后来者:“这里发生了什么,为什么发生,以及接下来该怎么办。” 这,才是Python作为一门工程语言,最迷人的地方。
