第 8 章 Python 中的 I/O

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. 系统生成一个 神秘数字,范围在 1 到 100 之间。
  2. 用户输入自己的猜测。
  3. 系统根据猜测反馈信息,更新提示范围,直到用户猜中数字。

这个示例不仅可以练习 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)游戏。游戏规则如下:

  1. 游戏棋盘为 3×3 网格,玩家轮流下棋。
  2. 玩家输入位置时,需要检查该位置是否已被占用。
  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 中,序列化和反序列化是非常常用的操作,尤其是在跨进程通信、网络编程以及数据存储中。常用的工具包括:

  1. pickle****模块
    pickle 可以序列化几乎所有 Python 对象,包括自定义类实例:

    import pickle

    class Book:
    def init(self, book_id, author, title):
    self.book_id = book_id
    self.author = author
    self.title = title

    book = Book(1, "鲁迅", "呐喊")

    序列化

    serialized_book = pickle.dumps(book)

    反序列化

    deserialized_book = pickle.loads(serialized_book)
    print(deserialized_book.title) # 输出:呐喊

  2. 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 可跨语言传输,但不支持复杂对象,需要额外处理。

通过掌握 pickleshelvejson,Python 程序可以根据不同场景选择合适的序列化工具,实现数据持久化、跨进程传输和跨平台数据交换。

相关推荐
micro_cloud_fly2 小时前
langchain langgraph历史会话的 json序列化
python·langchain·json
whitelbwwww2 小时前
Pytorch--张量
开发语言·pytorch·python
qy-ll2 小时前
Leetcode100题逐题详解
数据结构·python·学习·算法·leetcode
2301_764441332 小时前
基于python与Streamlit构建的卫星数据多维可视化分析
开发语言·python·信息可视化
陈奕昆2 小时前
n8n实战营Day3课时3:库存物流联动·全流程测试与异常调试
人工智能·python·n8n
weixin_457760002 小时前
DefaultCPUAllocator: can‘t allocate memory
python·神经网络
测试人社区-小明2 小时前
测试金字塔的演进:如何构建健康的自动化测试套件
python·测试工具·数据挖掘·pycharm·机器人·github·量子计算
敬往事一杯酒哈2 小时前
1.3 Ros2快速体验
python·ros2
杨超越luckly2 小时前
HTML应用指南:利用GET请求获取全国瑞思教育门店位置信息
前端·python·arcgis·html·门店数据