Lenyiin头像
关注

第5篇_Python文件操作与异常处理:程序与外界交互的桥梁

前言

在前四篇博客中,我们已经建立了完整的 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、elsefinally:让清理逻辑更清晰

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 内置了几十种异常类型,比如 ValueErrorTypeErrorFileNotFoundError 等。但在实际项目中,内置异常往往不够精确。

比如你在写一个网络爬虫,需要区分 “连接超时” 和 “页面不存在” 两种错误。如果都用 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 的作用:

  1. 标识目录是一个包:Python 看到 __init__.py,就知道 utils/ 不是一个普通文件夹,而是一个 Python 包
  2. 包的初始化代码:当 import utils 时,Python 会自动执行 __init__.py 中的代码
  3. 控制导入行为:可以设置 __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.pymemo.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.pymemo.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.pyopen()自动关闭文件,防止数据丢失
异常处理 try/exceptutils.py 加载和保存时文件损坏、路径错误时不崩溃
自定义配置 config.py集中管理路径和日期格式修改配置时只改一处
模块导入 importmemo.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__':区分"直接运行"和"被导入"两种模式
JSONjson.load() / json.dump()Python 对象与文本格式互转,实现数据持久化

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/mmlhbjk/article/details/163452316

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--