前言
在前四篇博客中,我们已经建立了完整的 Python 编程基础:
- 第1篇:搭建环境,写下第一行代码,理解变量与基础语法
- 第2篇:掌握 Python 的数据类型与运算符,建立 “数据” 的认知
- 第3篇:掌握条件语句、循环、函数,建立 “逻辑” 的认知
- 第4篇:深入面向对象编程,建立 “结构” 的认知
学到这里,你的 Python 代码已经能在内存中自由运转了——变量、函数、类、对象,全都在 RAM 里运行,程序结束就消失。
但真实世界的程序,不能只活在内存里。它需要:
- 读取配置文件,知道该连接哪个数据库,密码是什么
- 保存用户数据,下次启动还能继续用,而不是每次都重新输入
- 读写日志文件,出了问题好追查,谁在什么时候做了什么操作
- 导入别人写好的模块,站在巨人的肩膀上,不用重复造轮子
这就需要掌握文件操作和异常处理——这是 Python 程序与外界交互的两座桥梁。本篇我们就来系统学习这两个主题。
1、文件操作:程序与磁盘的对话
1.1、为什么需要文件操作?
让我们从一个生活场景说起。
想象一下,你正在开发一个学生成绩管理系统。用户输入了 50 名学生的成绩,系统计算出了平均分、最高分、最低调,并生成了报表。用户很满意,关掉了程序。
然后用户第二天再次打开程序——所有数据都不见了。50 名学生的成绩全部丢失,必须重新输入一遍。
这就是内存的问题:程序运行期间,数据存储在**内存(RAM)**中。内存的特点是 “易失性” —— 断电后数据全部消失,程序退出后数据也被操作系统回收。
那么,如何让数据在程序退出后依然存在?答案是:把数据写入磁盘文件。
磁盘文件是存储在硬盘、SSD 等持久存储设备上的数据单元。它们不会因为断电或程序退出而消失。只要你不主动删除,文件可以存在几年甚至几十年。
文件操作解决的就是 “数据持久化” 的问题:把内存中的数据写入磁盘(保存),以及从磁盘读取数据到内存(加载)。
1.2、理解文件路径:文件在哪里?
在学习文件操作之前,我们需要先理解一个基础概念:文件路径。
文件路径就是文件在操作系统中的 “地址”,告诉操作系统去哪里找到这个文件。
Windows 路径示例: C:\Users\admin\Documents\hello.txt
Linux/Mac 路径示例: /home/user/documents/hello.txt
路径分为两种:
- 绝对路径:从磁盘根目录开始的完整路径,如
C:\Users\admin\Documents\hello.txt - 相对路径:从当前工作目录开始的路径,如
hello.txt(表示当前目录下名为 hello.txt 的文件)
在 Python 中,我们可以使用 os.getcwd() 获取当前工作目录(即程序运行所在的位置):
import os
print(os.getcwd()) # 查看当前工作目录,例如 /home/user/projects
了解当前工作目录,可以帮助我们正确地指定文件路径,避免 “文件找不到” 的错误。
1.3、第一个文件程序:写入
好了,理论够了,让我们写点实际的代码。
# 第一次接触文件操作:把文字写入文件
# 新建一个文件,向里面写入内容
# 1. 打开文件
# open(文件名, 模式) 是 Python 内置的文件打开函数
# 第一个参数是文件名(路径),第二个参数是打开模式
# 模式 'w' 表示写入模式(write),如果文件不存在会创建,文件存在会清空内容!
# encoding='utf-8' 指定字符编码,中文环境下基本都要写这个
file = open('hello.txt', 'w', encoding='utf-8')
# 2. 写入内容
# write() 方法把字符串写入文件缓冲区,注意:它不会自动加换行符!
file.write('Hello, Python!\n') # \n 是 Unix/Linux/Mac 的换行符
file.write('这是我的第一个文件程序\n') # 每写一行内容末尾加 \n,换到下一行
# 3. 关闭文件
# 非常重要!关闭后数据才会真正保存到磁盘
# 如果不关闭就退出程序,可能只有部分数据被写入(数据丢失风险)
file.close()
print('文件写入成功!请查看 hello.txt 文件')
运行这段代码,你会看到当前目录下多了一个 hello.txt 文件,用记事本或编辑器打开,内容正是我们写入的两行文字。
逐行详细解析:
| 代码 | 含义 |
|---|---|
open('hello.txt', 'w', encoding='utf-8') | 以 “写入模式” 打开文件。'w' 表示 write,如果文件不存在,会在当前目录自动创建一个新文件。如果文件已经存在,原有内容会被全部清空,这是 'w' 模式的一个重要特性。encoding='utf-8' 确保中文能正确存储,不写这个参数可能导致中文乱码。 |
file.write('Hello, Python!\n') | 把字符串写入文件缓冲区。\n 是换行符,表示 “另起一行”。如果不加 \n,所有内容会连成一行。 |
file.close() | 关闭文件。这是一个必须养成的好习惯。关闭时,操作系统才会把缓冲区里的数据真正写入磁盘。如果忘记关闭,程序异常退出时数据可能丢失。 |
⚠️ 高危提醒:
'w'模式会覆盖已有文件!如果你打开一个已存在的文件并用'w'模式写入,原内容会全部消失。保险的做法是:先用os.path.exists()判断文件是否存在,或者使用'a'追加模式(后面会讲到)。
1.4、读取文件内容
写入了文件,接下来学习如何把文件内容读出来。
方式一:一次性读取全部内容
# 读取刚才写入的文件
# 1. 打开文件,模式改为 'r'(read,只读模式)
file = open('hello.txt', 'r', encoding='utf-8')
# 2. 读取全部内容
# read() 方法一次性读取整个文件内容,返回一个字符串
content = file.read()
print('文件内容如下:')
print(content)
# 3. 关闭文件
file.close()
read() 方法的特点:
- 一次性把整个文件加载到内存中
- 适合读取小文件(几 KB 到几 MB)
- 如果文件很大(几百 MB 或更大),会占用大量内存,不建议使用
方式二:逐行读取(推荐处理大文件)
# 逐行读取文件,这种方式适合任何大小的文件
# 即使文件有 1GB,逐行读取也不会一次性占用 1GB 内存
file = open('hello.txt', 'r', encoding='utf-8')
# 方法1:用 for 循环直接遍历文件对象(最常用、最推荐)
# 文件对象是可迭代的(Iterable),每次迭代返回一行内容(包含 \n)
print('--- 方法1: for 循环遍历 ---')
for line in file:
# 每行末尾已有换行符,print 默认也会换行
# 所以这里用 end='' 避免出现"双换行"(一行后面多一个空行)
print(line, end='')
file.close()
为什么要用 end=''? 来看一个例子:
# 如果文件内容是: "第一行\n第二行\n"
# 逐行读取时,每行都包含末尾的 \n
# 如果用 print(line),会先打印 "第一行\n",然后自动再换行一次
# 结果是:两行之间多了一个空行
# 用 print(line, end='') 就不让 print 自动换行,这样正好一行对应一行
# 实际上可以更简单地用 print(line.rstrip())
# rstrip() 去掉字符串末尾的空白字符(包括 \n),print 再自动换行,输出更干净
for line in open('hello.txt', 'r', encoding='utf-8'):
print(line.rstrip()) # 去掉末尾的换行符和空格,再用 print 的换行输出
方式三:用 readlines() 读取所有行到列表
# readlines() 一次性读取所有行,返回一个列表
# 每行是一个字符串,组成一个列表
file = open('hello.txt', 'r', encoding='utf-8')
lines = file.readlines() # 返回: ['Hello, Python!\n', '这是我的第一个文件程序\n']
file.close()
# enumerate 同时获得索引和内容
for i, line in enumerate(lines, start=1): # start=1 让序号从 1 开始,方便用户阅读
print(f'第{i}行: {line}', end='')
方式四:用 readline() 逐行手动读取
# readline() 每次只读取一行,调用一次读取一行
# 适合需要"读一行、处理一行、再读下一行"的场景
file = open('hello.txt', 'r', encoding='utf-8')
line1 = file.readline() # 读取第一行
line2 = file.readline() # 读取第二行
# 如果已到文件末尾,再次调用返回空字符串 ''
print(f'第一行: {line1}', end='')
print(f'第二行: {line2}', end='')
file.close()
1.5、文件的多种打开模式详解
Python 的 open() 函数支持多种打开模式,理解每种模式的行为是安全操作文件的基础。
| 模式 | 含义 | 文件不存在时 | 文件已存在时 |
|---|---|---|---|
'r' | 只读(read) | 报错 FileNotFoundError | 原样读取,不修改 |
'w' | 只写(write) | 自动创建新文件 | 清空内容,重新写入 |
'a' | 追加(append) | 自动创建新文件 | 在末尾追加,不修改原有内容 |
'x' | 新建写(create) | 自动创建新文件 | 报错(不会覆盖已有文件) |
'r+' | 读写 | 报错 | 从文件开头开始写入,覆盖原有内容 |
'a+' | 追加读写 | 自动创建 | 在末尾追加,可以读取 |
💡 建议:日常使用中,
'r'、'w'、'a'三个模式最常用。'x'适合 “安全创建” 场景(不想覆盖已有文件),'r+'和'a+'适合需要同时读写的情况。
# 示例1:追加模式——不覆盖原有内容,在末尾添加新内容
file = open('hello.txt', 'a', encoding='utf-8') # 'a' = append
file.write('\n这是我追加的一行!')
file.close()
# 示例2:x 模式——只创建新文件,已存在就报错(安全保护)
try:
file = open('safe_create.txt', 'x', encoding='utf-8')
file.write('新文件内容')
file.close()
print('文件创建成功')
except FileExistsError:
print('文件已存在,拒绝覆盖')
1.6、更好的写法:with 上下文管理器
前面我们写了 file.close(),这个习惯很好,但人总会忘记。而且,如果程序在 write() 和 close() 之间发生了错误(比如网络断了、磁盘满了、程序崩溃了),close() 根本执行不到,缓冲区里的数据就丢了。
Python 提供了 with 语句(上下文管理器),可以自动管理文件的打开与关闭 —— 无论程序正常结束还是出错了,with 块结束时文件一定会被关闭:
# 用 with 语句打开文件,自动关闭(即使出错也不会丢失数据)
# as file 是给文件对象起个别名,方便后面使用
with open('hello.txt', 'r', encoding='utf-8') as file:
content = file.read()
print(content)
# with 块结束后,file 会自动关闭,不需要手动调用 close()
print('文件已自动关闭,读取完毕!')
两种写法对比:
# ❌ 手动管理(容易出错,不推荐)
file = open('hello.txt', 'r', encoding='utf-8')
content = file.read()
file.close() # 如果上面的 read() 出错,这行不会执行,文件一直开着(资源泄露)
# ✅ with 管理(推荐,优雅安全)
with open('hello.txt', 'r', encoding='utf-8') as file:
content = file.read()
# 无论正常结束还是出异常,with 块结束时文件都会自动关闭
💡 强烈建议:所有文件操作都用
with语句!这是 Python 程序员的职业习惯,也是面试中展示"代码素养"的重要细节。
2、异常处理:让程序优雅地犯错
2.1、什么是异常?
程序运行中,难免会遇到各种错误情况:
- 读取一个不存在的文件 → Python 抛出
FileNotFoundError - 把字符串当数字做除法 →
TypeError - 整数除以零 →
ZeroDivisionError - 列表只有 3 个元素却要访问第 10 个 →
IndexError - 用户输入了 “abc” 而程序期待一个数字 →
ValueError
这些错误,在 Python 中被称为异常(Exception)。异常不是 “程序坏了”,而是 “程序遇到了预期之外的情况”。
默认行为:如果不对异常做任何处理,程序会打印错误追踪信息(traceback),然后直接崩溃退出。
# 演示:程序崩溃的情况
num = int(input('请输入一个整数: ')) # 如果用户输入 "hello",这里会抛出 ValueError
print(10 / num) # 如果用户输入 0,这里会抛出 ZeroDivisionError
print('这里永远执行不到') # 因为上面出错后程序就崩溃了
运行结果(用户输入 hello):
请输入一个整数: hello
Traceback (most recent call last):
File "/path/to/test.py", line 2, in <module>
num = int(input('请输入一个整数: '))
ValueError: invalid literal for int() with base 10: 'hello'
运行结果(用户输入 0):
请输入一个整数: 0
Traceback (most recent call last):
File "/path/to/test.py", line 3, in <module>
print(10 / num)
ZeroDivisionError: division by zero
异常处理的作用:拦截这些错误,给程序一个 “优雅撤退” 的机会——打印友好的提示、记录日志、保存数据,然后继续运行,而不是直接崩溃让用户看到一堆技术术语。
2.2、基本结构:try/except
Python 的异常处理使用 try/except 语句:
# 最基本的异常处理结构
try:
# 尝试执行这些代码
# 如果一切正常,except 块被跳过
result = 10 / 0 # 这行会触发 ZeroDivisionError
except ZeroDivisionError:
# 如果 try 块中抛出了 ZeroDivisionError,执行这里的代码
# 程序不会崩溃,而是跳到这里继续执行
print('错误:除数不能为零!')
result = None # 给一个安全的默认值,程序可以继续运行
# 程序继续往下走,不会因为异常而中断
print(f'计算结果: {result}') # 输出: 计算结果: None
逐行解析:
try:—— 缩进块内是我们 “尝试执行” 的代码。如果这些代码正常运行完毕,except块被完全忽略,程序继续往下走。except 错误类型:—— 捕获指定类型的异常。当try块中的代码抛出了这个类型的异常,Python 就会跳入except块执行。如果抛出的不是这个类型,异常会继续向外层传播(可能触发外层的try/except,或者导致程序崩溃)。- 程序不会崩溃,而是走到
except块,给用户一个友好的提示,然后继续运行。
2.3、捕获多种异常
一个操作可能触发多种不同的异常,需要分别处理:
# 让用户输入两个数,做除法
try:
a = int(input('请输入被除数: ')) # 可能报错:ValueError(用户输入了非数字)
b = int(input('请输入除数: ')) # 可能报错:ValueError
result = a / b # 可能报错:ZeroDivisionError(除数为零)
except ValueError:
# 用户输入了非数字字符串,无法转换为整数
print('错误:请输入有效的整数!')
except ZeroDivisionError:
# 除数是零,无意义
print('错误:除数不能为零!')
print('程序继续运行到这里...') # 没有异常处理,这行在出错时根本执行不到
Python 会按照 except 的顺序匹配:从上到下,如果抛出的异常类型匹配某个 except,就执行那个 except 块,后面的不再检查。
2.4、捕获所有异常(慎用!)
有时你不确定会抛出什么异常,可以用 Exception(所有异常类型的基类)来捕获:
try:
file = open('一个不存在的文件.txt', 'r', encoding='utf-8')
content = file.read()
file.close()
except Exception as e:
# Exception 是所有内置异常的基类,可以捕获任何异常
# as e 把异常对象取个名字,这样我们就能访问异常的详细信息
print(f'发生了错误: {type(e).__name__}: {e}')
# 输出类似: FileNotFoundError: [Errno 2] No such file or directory: '一个不存在的文件.txt'
空 except: 警告:
except:
# 捕获所有异常——这是一个非常危险的习惯!
# 因为它也会捕获 KeyboardInterrupt(用户按 Ctrl+C)、SystemExit(程序调用 sys.exit())等
# 可能导致程序无法正常退出,或者吞掉了关键的系统级异常
print('发生了未知错误!')
⚠️ 生产环境警示:空
except:或捕获Exception而不记录日志,是非常不好的习惯。它会让错误 “静默消失”,排查问题变得极其困难。正确做法是:捕获具体异常类型,或者至少用logging模块记录错误详情。
2.5、else 和 finally:让清理逻辑更清晰
try/except 还能配合两个额外的子句:
try:
num = int(input('请输入一个数字: '))
result = 100 / num
except ValueError:
print('输入无效,请输入数字!')
except ZeroDivisionError:
print('除数不能为零!')
else:
# 只有当 try 块中没有任何异常时,才执行 else 块
# 如果 try 中抛出了异常(被某个 except 捕获了),else 不会执行
# 这和 if/else 的语义不同!注意区分
print(f'✅ 计算成功,结果是 {result}')
finally:
# 无论有没有异常,finally 块一定会执行
# 常用于:关闭文件、关闭网络连接、释放锁、清理临时文件
print('--- 本次计算结束 ---')
finally 的典型应用场景:
# 场景:无论是否出错,都要确保某些清理工作完成
try:
with open('data.txt', 'w', encoding='utf-8') as f:
f.write('重要数据')
# 假设这里发生了意外错误
raise RuntimeError('模拟发生错误!') # raise 关键字用于主动抛出异常
except RuntimeError as e:
print(f'捕获到运行时错误: {e}')
finally:
# with 语句已经自动关闭了文件
# 但如果用的是手动 open(),就必须在 finally 中调用 close()
print('清理工作已完成(finally 执行了)')
2.6、主动抛出异常:raise
有时不是 “出了错”,而是 “检测到不合理的情况,需要告诉调用者这里出错了”。
def set_age(age):
"""设置年龄,要求 age 是 0~150 之间的整数"""
# isinstance() 是 Python 内置的类型检查函数
# 如果 age 不是 int 类型,说明传参错误
if not isinstance(age, int):
raise TypeError(f'年龄必须是整数类型,收到的是 {type(age).__name__}')
# 年龄超出合理范围,也是一种错误
if age < 0:
raise ValueError('年龄不能为负数!')
if age > 150:
raise ValueError('年龄不能超过 150!')
print(f'✅ 年龄设置为: {age}')
# 测试各种情况
print('测试1: 正常值')
set_age(25)
print('\n测试2: 负数')
try:
set_age(-5)
except ValueError as e:
print(f'捕获到数值错误: {e}')
print('\n测试3: 字符串')
try:
set_age('二十岁')
except TypeError as e:
print(f'捕获到类型错误: {e}')
运行结果:
测试1: 正常值
✅ 年龄设置为: 25
测试2: 负数
捕获到数值错误: 年龄不能为负数!
测试3: 字符串
捕获到类型错误: 年龄必须是整数类型,收到的是 str
raise 语句的原理:抛出异常后,Python 会立即停止当前函数的执行,并在调用栈中向上寻找最近的 except 来处理。如果一直找不到,程序最终崩溃并打印追踪信息。
3、自定义异常:让错误信息更有意义
3.1、为什么需要自定义异常?
Python 内置了几十种异常类型,比如 ValueError、TypeError、FileNotFoundError 等。但在实际项目中,内置异常往往不够精确。
比如你在写一个网络爬虫,需要区分 “连接超时” 和 “页面不存在” 两种错误。如果都用 Exception 捕获,你不知道具体是哪种情况,也就无法针对性地处理。
自定义异常让你可以为项目量身定制异常类型,每个异常携带具体的上下文信息。
3.2、定义一个自定义异常类
# 定义一个自定义异常类
# 规范1:类名通常以 Error 结尾(这是 Python 的社区惯例)
# 规范2:必须继承自 Python 的内置 Exception 类(或者其子类)
class AgeValueError(Exception):
"""
当年龄不在合理范围(0~150)时抛出此异常
这个异常比内置的 ValueError 更精确,可以帮助调用者做出更准确的处理
"""
def __init__(self, age, message='年龄必须在 0~150 之间'):
"""
构造函数:初始化异常对象
参数:
age: 出错时用户传入的年龄值(保存下来,方便调试时知道是什么值出错)
message: 错误提示信息
"""
self.age = age # 保存出错时的年龄值
self.message = message # 保存错误提示
super().__init__(self.message) # 调用父类构造函数,把错误信息传进去
def __str__(self):
"""
当这个异常被 print() 或转成字符串时,返回什么内容
例如打印 except 块中的 e 时,就会显示这里返回的字符串
"""
return f'AgeValueError: 年龄={self.age},错误原因: {self.message}'
# 使用自定义异常
def set_age(age):
if not isinstance(age, int):
raise AgeValueError(age, '年龄必须是整数类型(收到其他类型)')
if age < 0 or age > 150:
raise AgeValueError(age) # 使用默认错误信息
print(f'✅ 年龄设置为: {age}')
# 测试
try:
set_age(200)
except AgeValueError as e:
print(f'捕获到自定义异常: {e}')
print(f'出错的年龄值是: {e.age}') # 可以访问异常对象的属性
print(f'错误原因: {e.message}')
运行结果:
捕获到自定义异常: AgeValueError: 年龄=200,错误原因: 年龄必须在 0~150 之间
出错的年龄值是: 200
错误原因: 年龄必须在 0~150 之间
3.3、设计一个项目级异常体系
在实际项目中,我们通常会设计一个异常继承层次:
# 项目异常基类:所有业务异常都继承自这个类
# 这样调用者可以统一捕获 ProjectError,一次性处理所有业务异常
class ProjectError(Exception):
"""所有项目异常的基类,提供统一的父类型"""
pass
# 具体异常:每个异常代表一种具体的错误情况
class ConfigError(ProjectError):
"""配置错误:配置文件缺失、格式错误、权限不足"""
pass
class DataNotFoundError(ProjectError):
"""数据未找到:资源不存在,比如用户文件、数据库记录"""
pass
class ValidationError(ProjectError):
"""数据验证失败:输入参数不符合要求,比如邮箱格式错误、密码太短"""
pass
# 统一捕获的好处
try:
raise DataNotFoundError('用户 ID=123 的数据不存在')
except ProjectError as e:
# 捕获所有项目异常(ConfigError、DataNotFoundError、ValidationError 都会被捕获)
# 调用者可以选择统一处理,或者根据具体类型做不同处理
print(f'项目异常: {type(e).__name__} - {e}')
这个设计的好处是:调用者可以只写一个 except ProjectError,捕获所有业务异常,不用写多个 except;如果需要区分,也可以单独 except DataNotFoundError 做特殊处理。
4、模块与包:Python 代码的复用与组织
4.1、什么是模块?
“模块” 听起来高大上,其实就是一个 .py 文件。
在第 3 篇写的 def calculate_average(scores): 函数,如果放在 statistics.py 文件里,那 statistics.py 就是一个模块(Module),其中的函数可以被其他 .py 文件复用。
我们一直在用的 print()、input()、open()、len()、range() 是什么?它们来自哪里?
答案是:内置模块 builtins。Python 把最常用的函数和数据集中打包,放在 builtins 模块里,任何时候都可以直接使用。
# 查看 builtins 模块里有哪些东西
import builtins
print('内置函数和异常的数量:', len(dir(builtins)))
print('部分内置函数:', [x for x in dir(builtins) if not x.startswith('_')][:10])
# 输出类似: ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', ...]
4.2、import 的多种方式
方式一:导入整个模块(最常用)
# 导入数学模块(math 模块提供了平方根、幂函数、对数、三角函数等)
import math
# 使用模块里的函数和常量:模块名.成员名()
print(math.sqrt(16)) # 平方根 → 4.0
print(math.pi) # 圆周率 → 3.141592653589793
print(math.pow(2, 10)) # 2的10次方 → 1024.0
print(math.floor(3.7)) # 向下取整 → 3
print(math.ceil(3.2)) # 向上取整 → 4
print(math.log(100, 10)) # 以10为底100的对数 → 2.0
方式二:给模块起别名
# 有些模块名很长,每次写全称很麻烦,可以起一个简短的别名
import math as m # 之后用 m.sqrt() 代替 math.sqrt()
import numpy as np # 数据科学领域常见的别名
import pandas as pd # 同样是约定俗成的别名
print(m.sqrt(9)) # 3.0
print(np.array([1, 2, 3])) # NumPy 的数组对象
print(pd.DataFrame({'A': [1, 2]})) # Pandas 的数据帧对象
方式三:只导入需要的函数(减少命名空间污染)
# 如果你只用到 math 模块的 sqrt 和 pi,可以只导入这两个
# 之后直接写 sqrt(),不需要 math.sqrt()
from math import sqrt, pi
print(sqrt(16)) # 直接写 sqrt,不需要 math.sqrt
print(pi) # 直接写 pi,不需要 math.pi
方式四:从模块导入所有内容(不推荐)
# from math import * # 导入 math 所有内容
# 风险:如果你的代码里也定义了 sqrt,会覆盖 math 的 sqrt(命名冲突)
# 而且代码可读性差——读者不知道 sqrt 到底是你写的还是模块提供的
4.3、标准库模块巡礼:几个最常用的内置模块
Python 自带了一个极其丰富的标准库,无需安装,直接 import 就能用。标准库是 Python 最重要的优势之一。
4.3.1、os 模块——操作系统相关
import os
# 获取当前工作目录(程序运行所在的文件夹路径)
print('当前目录:', os.getcwd())
# 列出当前目录下的所有文件(类似命令行里的 ls)
print('当前文件列表:', os.listdir('.'))
# 创建目录
os.mkdir('test_dir') # 在当前目录创建一个叫 test_dir 的文件夹
print('目录已创建')
# 删除文件(注意:是永久删除,不会进回收站!)
if os.path.exists('hello.txt'):
os.remove('hello.txt')
print('文件已删除')
# 判断文件/目录是否存在
print('文件存在?', os.path.exists('hello.txt')) # False(刚删除)
print('目录存在?', os.path.exists('test_dir')) # True
# 拼接路径(跨平台正确拼接,不用手动加 / 或 \)
file_path = os.path.join('test_dir', 'data.txt')
print('拼接的路径:', file_path)
# 获取文件大小和修改时间
if os.path.exists('hello.txt'):
size = os.path.getsize('hello.txt')
mtime = os.path.getmtime('hello.txt')
print(f'文件大小: {size} 字节')
print(f'修改时间: {mtime}')
4.3.2、datetime 模块 —— 日期和时间
几乎所有程序都需要处理日期和时间:日志的时间戳、用户的注册时间、订单的过期时间等。
import datetime
# 获取当前时间(精确到微秒)
now = datetime.datetime.now()
print(f'当前时间: {now}')
print(f'年: {now.year}, 月: {now.month}, 日: {now.day}')
print(f'时: {now.hour}, 分: {now.minute}, 秒: {now.second}')
print(f'星期: {now.strftime("%A")}') # 英文星期几,如 "Monday"
# 格式化时间:把时间对象转成字符串
# strftime = string format time,把时间格式化成字符串
formatted = now.strftime('%Y-%m-%d %H:%M:%S') # 2026-05-25 12:30:45
print(f'格式化后的时间: {formatted}')
# 解析时间:把字符串转成时间对象
# strptime = string parse time,把字符串解析成时间
date_str = '2026-01-01 08:00:00'
parsed = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
print(f'解析后的时间: {parsed}')
print(f'距离今天: {(now - parsed).days} 天')
# 计算时间差(timedelta)
from datetime import timedelta
tomorrow = now + timedelta(days=1) # 明天
yesterday = now - timedelta(days=1) # 昨天
one_hour_later = now + timedelta(hours=1) # 一小时后
ten_minutes_ago = now - timedelta(minutes=10) # 十分钟前
print(f'明天: {tomorrow}')
print(f'十分钟前: {ten_minutes_ago}')
时间戳与 datetime 的互相转换:
import time
# 获取当前时间戳(从 1970-1-1 0:00:00 UTC 到现在经过的秒数)
timestamp = time.time()
print(f'当前时间戳: {timestamp}')
# 时间戳转 datetime
dt = datetime.datetime.fromtimestamp(timestamp)
print(f'时间戳转 datetime: {dt}')
# datetime 转时间戳
ts = dt.timestamp()
print(f'datetime 转时间戳: {ts}')
4.3.3、random 模块——随机数
随机数在游戏、抽奖、测试数据生成等场景中非常有用。
import random
# random.random() 返回 0.0 ~ 1.0 之间的随机小数(均匀分布)
print('随机小数:', random.random())
# random.randint(a, b) 返回 a ~ b 之间的随机整数(两端都包含)
print('随机整数(1~100):', random.randint(1, 100))
# random.choice(列表) 从列表中随机选一个元素
fruits = ['苹果', '香蕉', '橙子', '葡萄', '西瓜']
print('随机选一个:', random.choice(fruits))
# random.sample(列表, k) 从列表中随机选 k 个不重复的元素
print('随机选3个:', random.sample(fruits, 3))
# random.shuffle(列表) 把列表原地打乱(洗牌)
cards = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
random.shuffle(cards) # 洗牌
print('洗牌后:', cards)
# random.uniform(a, b) 返回 a ~ b 之间的随机小数
print('随机小数(1~10):', random.uniform(1, 10))
4.3.4、json 模块 —— 轻量级数据交换格式
JSON(JavaScript Object Notation)是一种广泛使用的数据格式,用于在不同编程语言之间交换数据。Python 内置的 json 模块可以轻松实现 Python 对象与 JSON 字符串之间的转换。
import json
# Python 数据结构(字典和列表)
data = {
'name': '小明',
'age': 20,
'score': 95.5,
'hobbies': ['篮球', '编程', '阅读']
}
# 序列化:把 Python 对象转成 JSON 字符串
json_str = json.dumps(data, ensure_ascii=False, indent=2)
# ensure_ascii=False 让中文字符正常显示(不转成 \u... 编码)
# indent=2 让输出格式化缩进,更容易阅读
print('序列化后的 JSON 字符串:')
print(json_str)
# 反序列化:把 JSON 字符串转回 Python 对象
parsed_data = json.loads(json_str)
print('反序列化后的 Python 对象:', parsed_data)
print(f'名字: {parsed_data["name"]}, 年龄: {parsed_data["age"]}')
4.4、什么是包?
当你的项目变大,一个 .py 文件装不下所有代码时,就需要包(Package)。
包 = 包含 __init__.py 文件的目录
my_project/ # 项目根目录(不是包,只是一个文件夹)
├── main.py # 主程序入口文件(不是包,是一个普通模块)
├── config.py # 配置模块(普通 .py 文件)
└── utils/ # 这是一个包(因为它是一个包含 __init__.py 的目录)
├── __init__.py # 包标识文件(Python 3.3+ 可省略,但建议保留)
├── string_utils.py # 字符串工具模块
├── file_utils.py # 文件操作模块
└── date_utils.py # 日期工具模块
__init__.py 的作用:
- 标识目录是一个包:Python 看到
__init__.py,就知道utils/不是一个普通文件夹,而是一个 Python 包 - 包的初始化代码:当
import utils时,Python 会自动执行__init__.py中的代码 - 控制导入行为:可以设置
__all__列表,控制from utils import *时导出哪些内容
# utils/__init__.py 示例
__all__ = ['list_files', 'read_file'] # 控制 from utils import * 的导出
from .file_utils import list_files, read_file # . 表示同目录
from .date_utils import format_date
# 也可以在这里初始化一些数据
APP_NAME = '我的备忘录'
VERSION = '1.0.0'
导入包中的模块:
# 方式1:导入整个包,再访问其中的模块(# 方式1:导入整个包,再访问其中的模块(最清晰,推荐)
import utils.file_utils
files = utils.file_utils.list_files('.')
# 方式2:给包或模块起别名(缩短名字)
import utils.file_utils as fu
files = fu.list_files('.')
# 方式3:从包中的模块直接导入具体函数
from utils.file_utils import list_files
print(list_files('.')) # 直接用函数名
4.5、__name__ 的魔法
每个 .py 文件都有一个内置变量 __name__,它揭示了模块的 “运行身份”:
- 直接运行这个文件时 →
__name__的值是'__main__' - 被其他文件 import 时 →
__name__的值是文件名本身
# utils/file_utils.py
def list_files(path):
"""列出目录下所有文件"""
import os
return os.listdir(path)
# 这一行表示:如果直接运行这个文件(而不是被导入),才执行测试代码
# 这是模块的 "自测入口",是 Python 模块的标准写法
if __name__ == '__main__':
print('直接运行 file_utils.py,进行自测...')
print(list_files('.'))
为什么要这样?
当你写一个模块,里面有测试代码时,直接运行可以测试功能,但当其他文件 import 它时,不想让测试代码跑一遍。if __name__ == '__main__': 就是这个作用 —— 它是模块的自测入口。
每个标准库和开源项目的源码中,你都能看到 if __name__ == '__main__': 的身影,这是 Python 程序员必须掌握的标准模式。
5、综合实战:带配置的备忘录程序
5.1、项目背景与目标
我们学了很多文件操作和异常处理的知识,现在来做一个小项目,把这些知识串联起来。
目标:写一个命令行备忘录程序,实现以下功能:
- 添加备忘录:用户输入内容,系统记录当前时间,保存到文件
- 查看备忘录:列出所有保存的备忘,带编号,方便阅读
- 删除备忘录:按编号删除某条备忘
- 数据持久化:关闭程序后重新打开,数据依然存在(JSON 文件存储)
- 配置分离:文件路径等可能变化的配置,集中在一个文件中管理
这个实战项目综合运用了本篇几乎所有知识点:文件读写、with 语句、异常处理、自定义异常、模块导入、if __name__ == '__main__'、JSON 序列化,是绝佳的练习材料。
5.2、项目结构
我们把代码拆分成 3 个文件,每个文件各司其职:
memo_app/
├── config.py # 配置文件:所有可能变化的常量集中在这里
├── utils.py # 工具模块:封装文件读写操作,业务逻辑与数据存储分离
└── memo.py # 主程序入口:处理用户交互逻辑
这样做的好处是:修改数据存储方式时,不需要改动主程序。比如今天用 JSON 存,明天想改成数据库,只需要修改 utils.py,memo.py 不需要任何改动。
5.3、代码实现
Step 1: 配置文件 config.py
"""
配置文件:把可能会变的值集中管理
以后要改路径,直接改这里,不用在代码里到处找
"""
# 备忘录数据文件存放路径(相对路径,相对于主程序运行目录)
MEMO_FILE = 'memo_db.json'
# 日期格式(用于显示每条备忘的记录时间)
DATE_FORMAT = '%Y-%m-%d %H:%M'
# 欢迎语
WELCOME_TEXT = '📝 备忘录 v1.0'
为什么要单独建一个配置文件?假设有一天要把 memo_db.json 改成存储在 /home/user/data/memo_db.json,只需要修改 config.py 中的一行,整个程序就生效了。如果不分离配置,就需要去 utils.py 和 memo.py 里到处找、到处改。
Step 2: 工具函数 utils.py
"""
工具模块:封装所有文件读写操作
把"数据如何保存"的逻辑集中在这里,主程序不需要知道数据存在哪个文件
"""
import json # Python 内置的 JSON 处理模块
import os # 用于判断文件是否存在
from datetime import datetime # 获取当前时间
from config import MEMO_FILE, DATE_FORMAT # 导入配置中的常量
def load_memos():
"""
从文件加载备忘录列表
如果文件不存在,说明是第一次运行,返回空列表
如果文件存在但格式损坏(不是合法的 JSON),返回空列表,不让程序崩溃
"""
# 先判断文件是否存在
if not os.path.exists(MEMO_FILE):
return [] # 文件不存在,返回空列表(不是错误,是正常情况)
try:
# with 语句:打开文件,自动关闭,防止资源泄露
with open(MEMO_FILE, 'r', encoding='utf-8') as f:
# json.load() 把文件内容(JSON 格式的文本)解析成 Python 对象(通常是字典或列表)
data = json.load(f)
# 确保返回的是列表,如果不是也返回空列表
if isinstance(data, list):
return data
else:
return []
except (json.JSONDecodeError, IOError, UnicodeDecodeError) as e:
# 文件损坏或读取失败,返回空列表,不让程序崩溃
# 这里捕获了多种可能的错误,确保任何问题都能优雅处理
print(f'⚠️ 加载备忘录失败(文件可能已损坏): {e}')
return []
def save_memos(memos):
"""
把备忘录列表保存到文件
json.dump 把 Python 对象序列化成 JSON 格式文本,写入文件
"""
try:
with open(MEMO_FILE, 'w', encoding='utf-8') as f:
# json.dump 序列化:Python 对象 -> JSON 文本 -> 写入文件
# ensure_ascii=False: 保存中文时不用 ASCII 转义(更可读)
# indent=2: 格式化缩进,每条备忘占两格,文件更容易阅读和手动编辑
json.dump(memos, f, ensure_ascii=False, indent=2)
except IOError as e:
# 保存失败时打印错误,但不让程序崩溃
# 实际项目中这里通常还会记录日志
print(f'⚠️ 保存备忘录失败: {e}')
def format_time():
"""返回当前时间字符串,格式由配置文件指定"""
return datetime.now().strftime(DATE_FORMAT)
# 自定义异常:当备忘录编号无效时抛出
class MemoIndexError(Exception):
"""当备忘录索引无效时抛出"""
pass
Step 3: 主程序 memo.py
"""
备忘录主程序
功能:添加/查看/删除备忘录,数据自动保存到文件
"""
import utils # 导入同目录下的 utils 模块(会自动执行 utils.py 的顶层代码)
import sys # 用于 sys.exit()
def show_menu():
"""显示操作菜单"""
print('\n--- 备忘录菜单 ---')
print('1. 添加备忘录')
print('2. 查看所有备忘录')
print('3. 删除备忘录')
print('4. 退出')
print('--------------------')
def add_memo(memos):
"""添加一条新备忘录"""
content = input('请输入备忘录内容: ').strip()
# 空内容不添加
if not content:
print('⚠️ 内容不能为空,请重新输入')
return memos
# 构建备忘录条目(字典格式,方便序列化为 JSON)
memo = {
'content': content, # 备忘内容
'time': utils.format_time() # 记录添加时间
}
memos.append(memo) # 添加到列表末尾
print(f'✅ 备忘已添加 [{memo["time"]}]')
return memos
def list_memos(memos):
"""显示所有备忘录"""
if not memos:
print('📭 还没有任何备忘录,添加一条吧!')
return
print(f'\n📋 共 {len(memos)} 条备忘录:')
print('-' * 50)
# enumerate 获得序号(从 0 开始,+1 变成从 1 开始)
for i, memo in enumerate(memos, start=1):
print(f' [{i}] {memo["time"]}')
print(f' {memo["content"]}')
print()
print('-' * 50)
def delete_memo(memos):
"""删除一条备忘录"""
if not memos:
print('📭 没有备忘录可删除')
return memos
list_memos(memos) # 先显示列表,让用户知道每条的编号
try:
# 让用户输入要删除的编号
choice = input('请输入要删除的编号 (输入 0 取消): ').strip()
choice = int(choice) # 转为整数,可能抛出 ValueError
if choice == 0:
print('已取消')
return memos
# 用户输入的是序号(从 1 开始),列表索引从 0 开始,所以要 -1
index = choice - 1
# 检查序号是否在有效范围内
if index < 0 or index >= len(memos):
print('⚠️ 序号超出范围,没有这条备忘录')
return memos
# 确认删除(避免误删)
confirm = input(f'确认删除 "{memos[index]["content"]}" 吗?(y/n): ')
if confirm.lower() == 'y':
deleted = memos.pop(index) # pop(index) 删除并返回指定位置的元素
print(f'🗑️ 已删除: {deleted["content"]}')
else:
print('已取消')
except ValueError:
print('⚠️ 请输入有效的数字序号')
return memos
def main():
"""主函数:程序入口"""
print('=' * 50)
print('📝 备忘录 v1.0 欢迎使用!')
print('=' * 50)
# 启动时:从文件加载已有备忘录(实现数据持久化)
memos = utils.load_memos()
print(f'📂 已加载 {len(memos)} 条备忘录')
# 主循环:让用户持续操作,直到选择退出
while True:
show_menu()
choice = input('请选择操作 (1-4): ').strip()
if choice == '1':
memos = add_memo(memos)
elif choice == '2':
list_memos(memos)
elif choice == '3':
memos = delete_memo(memos)
elif choice == '4':
# 退出时:保存数据到文件
utils.save_memos(memos)
print(f'📂 已保存 {len(memos)} 条备忘录,再见!')
break
else:
print('⚠️ 无效选项,请输入 1~4')
# 每次操作后自动保存(防止程序意外退出时数据丢失)
utils.save_memos(memos)
# 只有直接运行 memo.py 时才执行 main()
# 如果被其他文件 import,不会执行
# 这是 Python 程序的入口标准写法
if __name__ == '__main__':
main()
5.4、运行效果
==================================================
📝 备忘录 v1.0 欢迎使用!
==================================================
📂 已加载 0 条备忘录
--- 备忘录菜单 ---
1. 添加备忘录
2. 查看所有备忘录
3. 删除备忘录
4. 退出
--------------------
请选择操作 (1-4): 1
请输入备忘录内容: 下午3点开会
✅ 备忘已添加 [2026-05-25 12:05]
--- 备忘录菜单 ---
1. 添加备忘录
2. 查看所有备忘录
3. 删除备忘录
4. 退出
--------------------
请选择操作 (1-4): 1
请输入备忘录内容: 记得买牛奶
✅ 备忘已添加 [2026-05-25 12:06]
--- 备忘录菜单 ---
1. 添加备忘录
2. 查看所有备忘录
3. 删除备忘录
4. 退出
--------------------
请选择操作 (1-4): 2
📋 共 2 条备忘录:
--------------------------------------------------
[1] 2026-05-25 12:05
下午3点开会
[2] 2026-05-25 12:06
记得买牛奶
--------------------------------------------------
请选择操作 (1-4): 3
📋 共 2 条备忘录:
--------------------------------------------------
[1] 2026-05-25 12:05
下午3点开会
[2] 2026-05-25 12:06
记得买牛奶
--------------------------------------------------
请输入要删除的编号 (输入 0 取消): 2
确认删除 "记得买牛奶" 吗?(y/n): y
🗑️ 已删除: 记得买牛奶
请选择操作 (1-4): 4
📂 已保存 1 条备忘录,再见!
5.5、数据是如何持久化的?
备忘录程序关闭后,数据保存在 memo_db.json 文件里。打开这个文件,你会看到类似这样的内容:
[
{
"content": "下午3点开会",
"time": "2026-05-25 12:05"
}
]
下次启动程序时,load_memos() 函数会把这个 JSON 文件内容读回来,还原成 Python 的列表和字典,整个程序就这样拥有了"记忆"。
5.6、本实战涉及的知识点汇总
| 知识点 | 出现在哪里 | 为什么用 |
|---|---|---|
with 语句 | utils.py 中 open() | 自动关闭文件,防止数据丢失 |
异常处理 try/except | utils.py 加载和保存时 | 文件损坏、路径错误时不崩溃 |
自定义配置 config.py | 集中管理路径和日期格式 | 修改配置时只改一处 |
模块导入 import | memo.py 导入 utils | 代码分层,各司其职 |
if __name__ == '__main__' | memo.py 入口保护 | 被 import 时不执行主逻辑 |
| JSON 序列化 | json.dump 保存、json.load 加载 | Python 对象 <-> JSON 文本互转 |
os.path.exists() | load_memos() 判断文件是否存在 | 第一次运行时不报错 |
6、总结
本篇我们学习了 Python 程序与外界交互的两大核心能力:
核心知识点回顾
| 知识点 | 关键语法 | 一句话说明 |
|---|---|---|
| 文件写入 | open('f', 'w') + f.write() | 把数据从内存永久存入磁盘 |
| 文件读取 | open('f', 'r') + f.read() | 把数据从磁盘加载到内存 |
| 自动关闭 | with open(...) as f: | 无论是否出错,块结束时自动关闭文件 |
| 异常捕获 | try/except/else/finally | 拦截错误,让程序优雅地处理而非崩溃 |
| 主动抛出 | raise ExceptionType('msg') | 发现不合理情况时,主动通知调用者 |
| 自定义异常 | class MyError(Exception): ... | 为项目定制精确的异常类型 |
| 模块导入 | import module / from m import func | 复用他人写的代码 |
| 包结构 | 目录 + __init__.py + 模块文件 | 组织大型项目的代码结构 |
__name__ | if __name__ == '__main__': | 区分"直接运行"和"被导入"两种模式 |
| JSON | json.load() / json.dump() | Python 对象与文本格式互转,实现数据持久化 |
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/mmlhbjk/article/details/163452316



