1. 什么是 I/O?
(1)文件 I/O 基础:理解输入与输出
在编程领域,I/O(Input/Output,输入与输出) 是程序与外部世界进行数据交换的方式。程序可以从某个来源读取数据(输入),也可以将处理结果输出到某个目标位置(输出)。这些来源与目标可能是终端、网络资源,也可能是本地文件。
Python 中的文件 I/O 操作非常常见,例如读取 .txt 文本文件、解析 .csv 数据文件,或将程序结果输出成 .html 页面。本章重点介绍对 文本文件(.txt) 的基础操作。
(2)常用的文件处理函数
Python 提供了一组非常实用的文件操作函数,下面将按功能逐项介绍。
① 打开文件: open(filename)
文件操作的第一步永远是使用 open() 打开文件:
file = open(filename)
filename:文件名或路径- 返回值
file:文件对象,可用于读取、写入和移动指针等操作
打开文件后,Python 会在系统中创建一个文件资源句柄,因此使用完毕后必须关闭文件。关闭文件的作用将在后文单独说明。
② 读取全部内容: file.read()
file.read() 用于一次性读取文件的全部内容(或读取指定长度的字符)。
content = file.read()
适用于文件较小、可以安全读入内存的场景。
③ 按行读取: file.readline()
file.readline() 每次读取一行内容,并以字符串形式返回:
line = file.readline()
适用于逐行处理数据,尤其是大文件分析。
④ 读取所有行: file.readlines()
file.readlines() 会将文件的每一行作为一个元素存入列表:
lines = file.readlines()
结果通常类似:
['第一行\n', '第二行\n', '第三行\n']
适用于需要基于行列表进行批量处理的场景。
⑤ 调整指针位置: file.seek(offset)
文件读取过程中,Python 会维护一个"文件指针",指向当前读取的位置。
使用 file.seek(offset) 可以移动这个指针:
file.seek(0) # 回到文件开头
file.seek(20) # 跳到第 20 个字节
可用于重复读取文件开头或跳过指定部分内容。
⑥ 关闭文件: file.close()
完成操作后,必须关闭文件:
file.close()
为什么要关闭文件?
- 释放系统资源:每打开一个文件都会占用文件句柄,数量过多可能导致系统无法继续打开新文件。
- 确保数据写入完整:文件写入通常会使用缓冲区,未关闭文件可能导致数据未完全写入。
- 保持程序稳定性:长期占用文件对象可能引发不可再现的异常。
虽然小型脚本里不关闭文件似乎不会立即出问题,但在大型项目、后台服务、批量处理任务中,这种习惯会带来严重隐患。因此,无论程序大小,都要养成及时关闭文件的良好习惯。
2. 文件编码(Encoding)
在处理文本文件时,字符编码(Character Encoding) 是一个必须重点理解的基础概念。字符编码指的是:将自然语言中的文字映射为计算机能够存储、处理和传输的数字形式。也就是说,编码让计算机能够识别并正确显示人类语言的文本。
在不同语言环境、不同平台和不同历史阶段中,出现过许多字符编码标准。常见的编码包括:
- UTF-8:目前最通用、兼容性最好的编码方式,也是 HTML 的标准编码,能够覆盖全球语言,是现代系统的主流选择。
- ASCII:最早的字符编码标准,只支持英文和基本符号。
- GBK:Windows 系统常用的简体中文编码方式,在早期本地化系统中广泛使用,向下兼容 GB2312。
不同编码方式会以不同的字节规则存储字符,因此使用错误的编码读取文件时,往往会导致乱码或解码错误。
Python 与文件编码
在 Python 中,当我们以默认方式打开文件时:
file = open(filename)
Python 会根据当前操作系统的默认编码来读取文件内容:
- 在 Windows 系统中,默认编码通常为 GBK
- 在 Linux 和 macOS 等现代系统中,默认编码一般为 UTF-8
这就意味着:
如果你在一台 Windows 电脑上(默认 GBK)打开一个 UTF-8 编码的文件,而未指定编码,Python 就会用错误的方式解析内容,导致乱码。
因此,为了确保跨平台稳定性和避免乱码问题,强烈建议在打开文件时显式指定编码方式。例如,当我们确认一个文件是 UTF-8 编码时,可以这样写:
file = open(filename, encoding="utf-8")
显式指定编码是一种良好的编码习惯,不仅能够避免乱码,还能提高程序在不同系统上的可移植性。
3. with 语句与 open() 函数
(1)with 语句
在 Python 中,with 语句是一种用于简化资源管理和异常处理的语法结构。当我们使用 with 打开文件时,Python 会在代码块执行完毕后自动关闭文件对象,无论过程中是否发生异常,都能够保证文件句柄被正确释放。这不仅减少了显式调用 file.close() 的负担,也让代码更加简洁、可读性更强。
with 语句的基本用法如下:
with open(filename, encoding="utf-8") as file:
content = file.read()
在这个结构中:
- 当进入
with代码块时,open()会返回一个文件对象并绑定到变量file; - 当退出代码块时(无论是否发生异常),文件都会被自动关闭。
这种写法几乎是现代 Python 开发中处理文件的标准形式,推荐在任何需要打开文件的情境中使用。
(2)文件模式(File Modes)
在打开文件时,我们可以通过 open() 的第二个参数指定文件的打开模式。不同模式代表不同的用途和行为。以下是常用的几种文件模式:
"r"--- 读取(Read)
默认模式。仅用于读取文件内容;如果文件不存在,会抛出错误。"a"--- 追加(Append)
以追加模式打开文件,新的内容会添加到文件末尾;如果文件不存在,会自动创建一个新文件。"w"--- 写入(Write)
用于写入内容。如果文件不存在会创建新文件;如果文件已存在,其原有内容将被清空。"x"--- 创建(Create)
用于创建一个新的文件;如果目标文件已经存在,会抛出异常。
提示: 文件模式不仅仅限于这几种,还有许多其他模式(例如 "rb"、"wb" 用于处理二进制文件)。在实际开发中,建议快速完整阅读所有模式的说明,以便根据实际需求选择最合适的文件打开方式。
4. 删除文件与文件夹
在 Python 中,如果需要删除文件或文件夹,可以使用 os 模块。os 是 操作系统(Operating System) 的缩写,提供了丰富的与系统交互的功能。
常用的删除方法包括:
① 删除文件
import os
os.remove("example.txt")
使用 os.remove(filename) 可以删除指定的文件。
② 删除文件夹
import os
os.rmdir("example_folder")
使用 os.rmdir(foldername) 可以删除指定的文件夹,但有一个重要限制:该文件夹必须为空,否则会抛出异常。
提示: 如果需要删除非空文件夹,可以使用 Python 的 shutil****模块 提供的高级方法。该模块可以实现递归删除文件夹及其中所有内容。关于 shutil 模块,我们将在后续章节中详细讲解。
5. 用户输入
Python 提供了方便的方式来接收用户输入,使程序能够与用户进行交互。当程序执行到需要用户输入的地方时,操作系统会将程序置于阻塞状态。也就是说,程序会暂停运行,直到用户提供输入内容后才会继续执行。这样做是为了让 CPU 能够高效调度其他任务,同时保证程序能够正确获取用户输入。
(1)input() 函数
Python 中用于接收用户输入的核心函数是 input()。其基本用法如下:
user_input = input("请输入内容:")
- 如果
input()带有参数,该参数会显示在标准输出流中,用作提示信息。 - 函数会读取用户输入的一整行内容(直到按下回车键),并将其转换为字符串返回,赋值给变量。
示例:
name = input("请输入你的姓名:")
print("你好," + name + "!")
程序运行效果:
请输入你的姓名:张三
你好,张三!
(2)终止程序运行
在终端或命令行中,如果需要强制终止正在等待输入的程序,只需按下快捷键:
Ctrl + C
这会触发一个 KeyboardInterrupt 异常,从而中断程序运行。
6. 猜数游戏
为了巩固用户输入和条件判断的知识,我们可以编写一个简单的 猜数游戏。游戏规则如下:
- 系统生成一个 神秘数字,范围在 1 到 100 之间。
- 用户输入自己的猜测。
- 系统根据猜测反馈信息,更新提示范围,直到用户猜中数字。
这个示例不仅可以练习 input() 函数的使用,还能帮助理解循环和条件判断在实际场景中的应用。
(1)示例程序
import random
# 系统随机生成一个 1 到 100 的神秘数字
secret_number = random.randint(1, 100)
low = 1
high = 100
print("欢迎来到猜数游戏!请猜一个 1 到 100 之间的数字。")
while True:
guess = int(input(f"请输入你的猜测({low}-{high}):"))
if guess < secret_number:
print("太小了,请再试一次。")
low = max(low, guess + 1) # 更新范围下限
elif guess > secret_number:
print("太大了,请再试一次。")
high = min(high, guess - 1) # 更新范围上限
else:
print(f"恭喜你,猜对了!神秘数字是 {secret_number}。")
break
(2)程序解析
- 生成神秘数字
使用random.randint(1, 100)随机生成 1 到 100 的整数。 - 循环等待用户输入
使用while True:构建循环,不断接收用户的猜测。 - 条件判断与范围更新
-
- 如果用户输入小于神秘数字,提示"太小了",并更新下限
low。 - 如果用户输入大于神秘数字,提示"太大了",并更新上限
high。 - 如果猜中,输出祝贺信息,并使用
break退出循环。
- 如果用户输入小于神秘数字,提示"太小了",并更新下限
- 输入转换
input()返回的是字符串,使用int()将其转换为整数用于比较。
7. 井字棋游戏
为了练习 循环、条件判断、函数封装和用户输入,我们可以编写一个 终端井字棋(Tic-Tac-Toe)游戏。游戏规则如下:
- 游戏棋盘为 3×3 网格,玩家轮流下棋。
- 玩家输入位置时,需要检查该位置是否已被占用。
- 每次落子后,程序检查是否有玩家获胜或平局。
这个示例能够将前面学习的用户输入、循环与条件判断知识综合应用。
(1)程序设计步骤
实现井字棋游戏可以按照以下步骤:
① 显示游戏棋盘
使用二维列表或简单的一维列表表示棋盘状态,并通过打印显示给用户。
② 接收用户输入
提示用户输入行列或编号,判断输入是否合法且该位置是否为空。
③ 更新棋盘
根据用户输入更新棋盘状态。
④ 获胜判定算法
检查行、列和对角线是否有相同符号连续三个,以判断胜利。
⑤ 完善游戏机制
- 确保玩家不能覆盖已有棋子
- 当棋盘被填满且无人获胜时判定为平局
(2)示例程序
def print_board(board):
for row in board:
print(" | ".join(row))
print("-" * 9)
def check_winner(board, player):
# 检查行
for row in board:
if all(cell == player for cell in row):
return True
# 检查列
for col in range(3):
if all(board[row][col] == player for row in range(3)):
return True
# 检查对角线
if all(board[i][i] == player for i in range(3)) or \
all(board[i][2-i] == player for i in range(3)):
return True
return False
def is_full(board):
return all(cell != " " for row in board for cell in row)
# 初始化棋盘
board = [[" " for _ in range(3)] for _ in range(3)]
players = ["X", "O"]
current_player = 0
print("欢迎来到井字棋游戏!")
print_board(board)
while True:
try:
move = input(f"玩家 {players[current_player]} 的回合,请输入行和列(例如 1 2):")
row, col = map(int, move.split())
row -= 1 # 将输入转换为索引
col -= 1
if board[row][col] != " ":
print("该位置已被占用,请重新输入。")
continue
board[row][col] = players[current_player]
print_board(board)
if check_winner(board, players[current_player]):
print(f"恭喜玩家 {players[current_player]} 获胜!")
break
if is_full(board):
print("平局!")
break
current_player = 1 - current_player # 切换玩家
except (ValueError, IndexError):
print("输入格式错误或超出范围,请输入行和列数字(1-3)")
(3)程序解析
① 棋盘表示
使用二维列表 board 存储棋盘状态,空格 " " 表示空位置。
② 显示棋盘
print_board() 函数通过行与列打印棋盘,并用竖线和横线分隔。
③ 玩家输入
使用 input() 接收玩家输入,并使用 split() 和 map() 转换为整数索引。
④ 合法性检查
- 判断输入是否在 1~3 的范围内
- 判断所选位置是否为空
⑤ 胜利判定
check_winner() 检查行、列和对角线是否有同一玩家连续三个棋子。
⑥ 平局判定
is_full() 检查棋盘是否已满而无人获胜。
⑦ 玩家轮换
使用 current_player = 1 - current_player 实现玩家交替。
8. 序列化与反序列化
在现代软件开发中,不同设备或不同程序之间的数据交换是非常常见的需求。比如,一个移动应用程序需要将用户生成的数据发送到云端服务器,或者两个独立运行的程序需要共享同一份数据。在这种情况下,程序之间必须通过某种媒介进行通信,例如:
- 文件系统:将数据写入文件,再由另一个程序读取
- 网络连接:通过 TCP 或 UDP 协议发送数据
- 消息队列或数据库:作为中间存储和交换的数据通道
然而,无论是哪种媒介,它们都只能识别比特流,也就是说,所有的数据在传输时最终都要以二进制的形式存在。举例来说,当一个应用程序希望发送数值 10 时,实际上发送的是它的二进制表示 1010,同时还需要附加一些信息,以便接收方能够正确解析这段比特流。
(1)复杂对象的交换
对于基本数据类型(如整数、浮点数或字符串),直接传输通常足够。但当两个应用程序需要交换自定义对象时,情况就更复杂了。例如,假设我们定义了一个 Book 类,并希望在两个程序间传递 Book 对象:
public class Book {
Book() { }
public long BookId { get; set; }
public string Author { get; set; }
public string Title { get; set; }
}
由于 Book 是自定义的复合数据类型,它不能直接以比特流形式传输。如果直接发送对象,接收方无法识别对象内部结构,也就无法还原对象内容。
这时,序列化(Serialization)就发挥了关键作用。
(2)序列化与反序列化的概念
① 序列化(Serialization)
序列化是指将对象转换为可传输的格式(如二进制、JSON、XML 等)的过程。通过序列化,可以定义对象在网络、文件或其他媒介上的表示规则,从而确保数据在传输过程中完整、安全且可解析。
序列化后的数据通常以字节流形式存在,便于在不同程序或不同设备之间传输。
② 反序列化(Deserialization)
反序列化是序列化的逆过程,即根据接收到的二进制或文本数据,重新构建原始对象。在上例中,接收方程序通过反序列化,将比特流解析回 Book 对象,从而能够像操作本地对象一样使用它。
Python 中的序列化
在 Python 中,序列化和反序列化是非常常用的操作,尤其是在跨进程通信、网络编程以及数据存储中。常用的工具包括:
-
pickle****模块
pickle可以序列化几乎所有 Python 对象,包括自定义类实例:import pickle
class Book:
def init(self, book_id, author, title):
self.book_id = book_id
self.author = author
self.title = titlebook = Book(1, "鲁迅", "呐喊")
序列化
serialized_book = pickle.dumps(book)
反序列化
deserialized_book = pickle.loads(serialized_book)
print(deserialized_book.title) # 输出:呐喊 -
json****模块
对于简单对象或字典类型,JSON 是更通用的序列化格式,便于跨语言传输:import json
book_dict = {"book_id": 1, "author": "鲁迅", "title": "呐喊"}
序列化为 JSON 字符串
json_str = json.dumps(book_dict)
反序列化为 Python 对象
data = json.loads(json_str)
print(data["title"]) # 输出:呐喊
提示 :pickle 可以处理复杂对象,但只能在 Python 环境中使用;json 可跨语言传输,但只支持基本数据类型,需要手动处理自定义对象。
(3)应用场景
- 跨程序通信:两个不同程序共享数据
- 数据持久化:将对象保存到文件或数据库
- 网络传输:将对象发送到远程服务器或客户端
- 缓存系统:序列化对象存入内存数据库(如 Redis)
通过掌握序列化与反序列化技术,程序可以在不同设备、不同语言环境之间安全、可靠地交换数据,同时保持对象结构和数据完整性。
9. Python 中的序列化工具
Python 提供了多种内置模块用于对象的序列化和反序列化操作,适用于不同场景。主要包括 pickle、shelve 和 json,每种工具各有特点和适用范围。
(1)Pickle 模块
Python 内置的 pickle 模块支持对几乎所有 Python 对象进行 序列化(Serialization) 与 反序列化(Deserialization)。通过 pickle,对象可以被转换为二进制形式,便于存储或跨程序传输。
① 使用注意事项
由于 pickle 操作的是二进制数据,因此在打开文件时应使用:
-
rb:以二进制方式读取 -
wb:以二进制方式写入import pickle
data = {"name": "Alice", "age": 25}
序列化到文件
with open("data.pkl", "wb") as f:
pickle.dump(data, f)从文件反序列化
with open("data.pkl", "rb") as f:
loaded_data = pickle.load(f)print(loaded_data) # 输出:{'name': 'Alice', 'age': 25}
② 常见使用场景
- 程序状态持久化:保存变量或程序状态,以便程序重启后从上次中断的位置继续运行。
- 跨进程/分布式系统传输:通过 TCP 连接传输 Python 数据。
- 数据库存储:将 Python 对象存储到不支持对象类型的数据库中,例如将字典对象序列化后存入 SQL 数据库。
⚠️ 安全提示:不要从不可信来源加载 pickle 文件,因为反序列化过程中可能执行任意代码。
(2)Shelve 模块
Shelve 模块是基于 pickle 实现的,提供了一种 "序列化字典" 机制。它允许将对象序列化后与一个键(字符串类型)关联,像操作字典一样访问已归档的数据。
import shelve
# 保存对象到 shelve 文件
with shelve.open("data_shelf") as db:
db["user1"] = {"name": "Alice", "age": 25}
# 读取对象
with shelve.open("data_shelf") as db:
user = db["user1"]
print(user) # 输出:{'name': 'Alice', 'age': 25}
Shelve 的优势:
- 适合保存大量对象,不必一次性全部加载到内存中。例如保存 3 万条元组数据时,使用 shelve 可以按需取用数据。
- 是关系型数据库不必要时的轻量级 Python 对象持久化方案。可以理解为 简易 Python 数据库。
- 任何可以被
pickle序列化的对象,都能存储在 shelve 中。
⚠️ 安全提示:Shelve 底层依赖 pickle,从不可信来源加载 shelf 文件存在安全风险。
(3)JSON 模块
对于简单对象或字典类型,json 是更通用的序列化格式,特别适合 跨语言或跨平台传输数据。
import json
book_dict = {"book_id": 1, "author": "鲁迅", "title": "呐喊"}
# 序列化为 JSON 字符串
json_str = json.dumps(book_dict)
# 反序列化为 Python 对象
data = json.loads(json_str)
print(data["title"]) # 输出:呐喊
JSON 的特点:
- 可以在不同编程语言之间传输数据,兼容性强。
- 只支持基本数据类型,如字典、列表、字符串、数字等。
- 对于自定义类对象,需要手动定义序列化与反序列化方法(例如使用
__dict__或自定义编码器)。
对比说明:
pickle可处理复杂对象,但仅限 Python 环境。json可跨语言传输,但不支持复杂对象,需要额外处理。
通过掌握 pickle、shelve 和 json,Python 程序可以根据不同场景选择合适的序列化工具,实现数据持久化、跨进程传输和跨平台数据交换。