《100天精通Python——基础篇 2025 第19天:玩转文件与目录,打通Python数据持久化任督二脉》

在 Python 程序运行期间,可以使用变量临时存储数据,但是当程序运行结束后,所有数据都将丢失。如果要永久保存数据,需要用到数据库或者文件。数据库适合保存表格化、关联性的数据,而文件适合保存松散的文本信息,或者图片、音视频等独立文件。Python 内置了文件和目录操作模块,可以很方便地读、写文件内容,实现数据的长久保存。

【学习重点】

  1. 文件的创建、读写和修改
  2. 文件的复制、删除和重命名
  3. 文件内容的搜索和替换
  4. 文件的比较
  5. 配置文件的读写
  6. 目录的创建和遍历

一、I/O操作概述

I/O 在计算机中是指 Input/Output,也就是 Stream(流) 的输入和输出。这里的输入和输出是相对于内存来说的,Input Stream(输入流)是指数据从外(磁盘、网络)流进内存,Output Stream 是数据从内存流出到外面(磁盘、网络)。程序运行时,数据都是在内存中驻留,由 CPU 这个超快的计算核心来执行,涉及到数据交换的地方(通常是磁盘、网络操作)就需要 IO 接口。那么这个 IO 接口是由谁提供呢?高级编程语言中的 IO 操作是如何实现的呢?操作系统是个通用的软件程序,其通用目的如下:

  1. 硬件驱动
  2. 进程管理
  3. 内存管理
  4. 网络管理
  5. 安全管理
  6. I/O 管理

操作系统屏蔽了底层硬件,向上提供通用接口。因此,操作 I/O 的能力是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来供开发者使用,Python 也不例外。

二、文件读写实现原理与操作步骤

2.1 文件读写实现原理

文件读写就是一种常见的 IO 操作。那么根据上面的描述,可以推断 Python 也应该封装操作系统的底层接口,直接提供了文件读写相关的操作方法。事实上,也确实如此,而且 Java、PHP 等其他语言也是。那么我们要操作的对象是什么呢?我们又如何获取要操作的对象呢?由于操作 I/O 的能力是由操作系统提供的,且现代操作系统不允许普通程序直接操作磁盘,所以读写文件时需要请求操作系统打开一个对象(通常被称为文件描述符-- file descriptor, 简称 fd),这就是我们在程序中要操作的文件对象。

通常高级编程语言中会提供一个内置的函数,通过接收 "文件路径" 以及 "文件打开模式" 等参数来打开一个文件对象,并返回该文件对象的文件描述符。因此通过这个函数我们就可以获取要操作的文件对象了。这个内置函数在 Python 中叫 open(),在 PHP 中叫 fopen()。

2.2 文件读写操作步骤

不同的编程语言读写文件的操作步骤大体都是一样的,都分为以下几个步骤:

  1. 打开文件,获取文件描述符
  2. 操作文件描述符–读/写
  3. 关闭文件

只是不同的编程语言提供的读写文件的 api 是不一样的,有些提供的功能比较丰富,有些比较简陋。需要注意的是:文件读写操作完成后,应该及时关闭。一方面,文件对象会占用操作系统的资源;另外一方面,操作系统对同一时间能打开的文件描述符的数量是有限制的,在 Linux 操作系统上可以通过 ulimit -n 来查看这个显示数量。如果不及时关闭文件,还可能会造成数据丢失。因为我将数据写入文件时,操作系统不会立刻把数据写入磁盘,而是先把数据放到内存缓冲区异步写入磁盘。当调用 close 方法时,操作系统会保证把没有写入磁盘的数据全部写到磁盘上,否则可能会丢失数据。拓展:

文件描述符(File Descriptor, FD):1.每打开一个文件(或套接字等 IO 资源),操作系统就会为这个文件分配一个文件描述符。2.文件描述符是一个整数,进程内部用它来引用内核为该文件维护的数据结构(如文件状态、读写位置等)。3.如果打开文件后不关闭,它会一直占用一个 FD,并且文件在内核中也保持打开状态(包括对文件锁、缓存、缓冲区等资源的占用)。文件描述符是进程级别的。每个进程有自己的一套文件描述符表(通常是一个数组或列表),索引是 FD(如 0、1、2 通常是 stdin、stdout、stderr)。每个进程打开同一个文件,会在自己进程的文件描述符表中占用一个新的 FD,且通常 FD 数值不同。它们各自独立维护自己的 "文件偏移位置" 和缓冲区。但所有进程打开的文件都会关联到系统级的全局文件表,这个全局表再关联到具体的磁盘 inode 等信息。文件描述符有数量限制吗?有,而且是有限的。对单个进程: 通常默认是 1024 个 FD,可以通过 ulimit -n 查看(在类 Unix 系统上)。可以通过 ulimit -n 4096(临时)或配置文件(如 /etc/security/limits.conf)增加。对系统整体: 操作系统也有限制整个系统中可打开的文件数量,这取决于内核参数,比如 Linux 中 /proc/sys/fs/file-max。

三、文件基本操作

3.1 前置知识

3.1.1 什么是文件路径,Python中如何书写文件路径?

关于文件,它有两个关键属性,分别是 "文件名""路径"其中,文件名指的是为每个文件设定的名称,而路径则用来指明文件在计算机上的位置。 例如,我的 Win10 笔记本上有一个文件名为 1.txt(句点之后的部分称为文件的 "扩展名",它指出了文件的类型),它的路径在 E:\file\temp,也就是说,该文件位于 E 盘下 file 文件夹中 temp 子文件夹下。

通过文件名和路径可以分析出,1.txt 是一个文本文件,file 和 temp 都是指 "文件夹"(也称为目录)。文件夹可以包含文件和其他文件夹,例如 1.txt 在 temp 文件夹中,该文件夹又在 file 文件夹中。注意,路径中的 E:\ 指的是 "根文件夹",它包含了所有其他文件夹。在 Windows 中,根文件夹名为 E:\,也称为 E: 盘。在 OS X 和 Linux 中,根文件夹是 /。考虑到大部分初学者,文章都使用的是 Windows 风格的根文件夹,如果你在 OS X 或 Linux 上输入交互式环境的例子,请用 / 代替。

另外,附加卷(诸如 DVD 驱动器或 USB 闪存驱动器),在不同的操作系统上显示也不同。在 Windows 上,它们表示为新的、带字符的根驱动器。诸如 D:\ 或 E:\。在 OS X 上,它们表示为新的文件夹,在 /Volumes 文件夹下。在 Linux 上,它们表示为新的文件夹,在 /mnt 文件夹下。同时也要注意,虽然文件夹名称和文件名在 Windows 和 OS X 上是不区分大小写的,但在 Linux 上是区分大小写的。Windows 上的反斜杠以及 OS X 和 Linux 上的正斜杠,在 Windows 上,路径书写使用反斜杠 "\" 作为文件夹之间的分隔符。但在 OS X 和 Linux 上,使用正斜杠 "/" 作为它们的路径分隔符。如果想要程序运行在所有操作系统上,在编写 Python 脚本时,就必须处理这两种情况。

好在,用 os.path.join() 函数来做这件事很简单。如果将单个文件和路径上的文件夹名称的字符串传递给它,os.path.join() 就会返回一个文件路径的字符串,包含正确的路径分隔符。在交互式环境中输入以下代码:

In [3]: import os

In [4]: os.path.join('temp', '1.txt')
Out[4]: 'temp\\1.txt'

因为此程序是在 Windows 上运行的,所以 os.path.join('temp', '1.txt') 返回 'temp\\1.txt'(请注意,反斜杠有两个,因为每个反斜杠需要由另一个反斜杠字符来转义)。如果在 OS X 或 Linux 上调用这个函数,该字符串就会是 'temp/1.txt'

[root@VM-16-6-centos ~]# python3
Python 3.8.10 (default, Nov  9 2024, 08:11:13) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.path.join("temp", "1.txt")
'temp/1.txt'

3.1.2 Python绝对路径和相对路径详解

在介绍绝对路径和相对路径之前,先要了解一下什么是当前工作目录。每个运行在计算机上的程序,都有一个 "当前工作目录"(或 cwd)。所有没有从根文件夹开始的文件名或路径,都假定在当前工作目录下。注意,虽然文件夹是目录的更新的名称,但当前工作目录(或当前目录)是标准术语,没有当前工作文件夹这种说法。在 Python 中,利用 os.getcwd() 函数可以取得当前工作路径的字符串,还可以利用 os.chdir() 改变它。例如,在交互式环境中输入以下代码:

E:\projects\pyCode2025>ipython
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.32.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import os

In [2]: os.getcwd()
Out[2]: 'E:\\projects\\pyCode2025'

In [3]: os.chdir('E:\\projects\\pyCode2025\\base')

In [4]: os.getcwd()
Out[4]: 'E:\\projects\\pyCode2025\\base'

可以看到,原本当前工作路径为 'E:\\projects\\pyCode2025',通过 os.chdir() 函数,将其改成了 'E:\\projects\\pyCode2025\\base'。需要注意的是,如果使用 os.chdir() 修改的工作目录不存在,Python 解释器会报错,例如:
在这里插入图片描述
了解了当前工作目录的具体含义之后,接下来介绍绝对路径和相对路径各自的含义和用法。

什么是绝对路径与相对路径? 明确一个文件所在的路径,有 2 种表示方式,分别是:

  1. 绝对路径:总是从根文件夹开始,Window 系统中以盘符(C: 、D:)作为根文件夹,而 OS X 或者 Linux 系统中以 / 作为根文件夹。
  2. 相对路径:指的是文件相对于当前工作目录所在的位置。例如,当前工作目录为 "E:\\projects\\pyCode2025\\base",若文件 test.py 就位于这个 base 文件夹下,则 test.py 的相对路径表示为 ".\test.py"(其中 .\ 就表示当前所在目录)。

在使用相对路径表示某文件所在的位置时,除了经常使用 .\ 表示当前所在目录之外,还会用到 ..\ 表示当前所在目录的父目录。图例:

以上图为例,如果当前工作目录设置为 C:\bacon,则这些文件夹和文件的相对路径和绝对路径,就对应为该图右侧所示的样子。Python os.path 模块(Python 常用模块(三):os.path模块 👉 查看文章)提供了一些函数,可以实现绝对路径和相对路径之间的转换,以及检查给定的路径是否为绝对路径,比如说:

  1. 调用 os.path.abspath(path) 将返回 path 参数的绝对路径的字符串,这是将相对路径转换为绝对路径的简便方法
  2. 调用 os.path.isabs(path),如果参数是一个绝对路径,就返回 True,如果参数是一个相对路径,就返回 False。
  3. 调用 os.path.relpath(path, start) 将返回从 start 路径到 path 的相对路径的字符串。如果没有提供 start,就使用当前工作目录作为开始路径。
  4. 调用 os.path.dirname(path) 将返回一个字符串,它包含 path 参数中最后一个斜杠之前的所有内容
  5. 调用 os.path.basename(path) 将返回一个字符串,它包含 path 参数中最后一个斜杠之后的所有内容。

在交互式环境中尝试上面提到的函数:

In [7]: os.getcwd()
Out[7]: 'E:\\projects\\pyCode2025\\base'

In [8]: os.path.abspath('.')
Out[8]: 'E:\\projects\\pyCode2025\\base'

In [9]: os.path.abspath('.\\day13-miniweb框架')
Out[9]: 'E:\\projects\\pyCode2025\\base\\day13-miniweb框架'

In [10]: os.path.isabs('.')
Out[10]: False

In [11]: os.path.isabs(os.path.abspath('.'))
Out[11]: True

In [12]: os.path.relpath('E:\\projects\\pyCode2025\\', 'E:\\')
Out[12]: 'projects\\pyCode2025'

In [13]: os.path.relpath('E:\\', 'E:\\projects\\pyCode2025\\')
Out[13]: '..\\..'

In [14]: temp_path = r'E:\projects\pyCode2025\base\test_new.txt'

In [15]: os.path.basename(temp_path)
Out[15]: 'test_new.txt'

In [16]: os.path.dirname(temp_path)
Out[16]: 'E:\\projects\\pyCode2025\\base'

注意,由于读者的系统文件和文件夹可能与我的不同,所以读者不必完全遵照本节的例子,根据自己的系统环境对本节代码做适当调整即可。\ 在 Python 中是转义字符,所以时常会有冲突。为了避坑,Windows 的绝对路径通常要稍作处理,可以写成以下两种方式:

# 会出问题这样写
temp_path = 'E:\projects\pyCode2025\base\test_new.txt'
temp_path = 'E:\\projects\\pyCode2025\\base\\test_new.txt'
#将'\'替换成'\\'

temp_path = r'E:\projects\pyCode2025\base\test_new.txt'
#在路径前加上字母r

除此之外,如果同时需要一个路径的目录名称和基本名称,就可以调用 os.path.split() 获得这两个字符串的元组,例如:

In [14]: temp_path = r'E:\projects\pyCode2025\base\test_new.txt'
In [17]: os.path.split(temp_path)
Out[17]: ('E:\\projects\\pyCode2025\\base', 'test_new.txt')

注意,可以调用 os.path.dirname() 和 os.path.basename(),将它们的返回值放在一个元组中,从而得到同样的元组。但使用 os.path.split() 无疑是很好的快捷方式。同时,如果提供的路径不存在,许多 Python 函数就会崩溃并报错,但好在 os.path 模块提供了以下函数用于检测给定的路径是否存在,以及它是文件还是文件夹:

  1. 如果 path 参数所指的文件或文件夹存在,调用 os.path.exists(path) 将返回 True,否则返回 False。
  2. 如果 path 参数存在,并且是一个文件,调用 os.path.isfile(path) 将返回 True,否则返回 False。
  3. 如果 path 参数存在,并且是一个文件夹,调用 os.path.isdir(path) 将返回 True,否则返回 False。

下面是在交互式环境中尝试这些函数的结果:

E:\projects\pyCode2025>ipython
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.32.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import os

In [3]: os.path.exists(r'E:\projects')
Out[3]: True

In [4]: os.path.exists('e:/temp')
Out[4]: False

In [5]: os.path.isdir('.')
Out[5]: True

In [6]: os.path.isdir('./test.py')
Out[6]: False

In [7]: os.path.isfile('./test.py')
Out[7]: True

3.1.3 Python文件基本操作

Python 中,对文件的操作有很多种,常见的操作包括创建、删除、修改权限、读取、写入等,这些操作可大致分为以下 2 类:

  1. 删除、修改权限:作用于文件本身,属于系统级操作
  2. 写入、读取:是文件最常用的操作,作用于文件的内容,属于应用级操作

其中,对文件的系统级操作功能单一,比较容易实现,可以借助 Python 中的专用模块(os、sys 等),并调用模块中的指定函数来实现。例如,假设如下代码文件的同级目录中有一个文件 "test.py",通过调用 os 模块中的 remove 函数,可以将该文件删除,具体实现代码如下:

E:\projects\pyCode2025\base>ipython
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.32.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import os

In [2]: os.remove('test.py')
# 文件不存在报错
In [3]: os.remove('test.py')
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 os.remove('test.py')

FileNotFoundError: [WinError 2] 系统找不到指定的文件。: 'test.py'

而对于文件的应用级操作,通常需要按照固定的步骤进行操作,且实现过程相对比较复杂,在 《2.2 文件读写操作步骤》 小节中已经讲解过,此处细化一下与 Python 对应起来,文件的应用级操作可以分为以下 3 步,每一步都需要借助对应的函数实现:

  1. 打开文件:使用 open() 函数,该函数会返回一个文件对象
  2. 对已打开文件做读/写操作:读取文件内容可使用 read()、readline() 以及 readlines() 函数;向文件中写入内容,可以使用 write() 函数。
  3. 关闭文件:完成对文件的读/写操作之后,最后需要关闭文件,可以使用 close() 函数。

再次强调: 一个文件,必须在打开之后才能对其进行操作,并且在操作结束之后,还应该将其关闭,这 3 步的顺序不能打乱。以上操作文件的各个函数,会各自作为一节在后续文章中进行详细介绍。

3.2 Python open()函数详解

我们先来看下在 Python、PHP 和 C语言中打开文件的函数定义:

# Python
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

# PHP
resource fopen(string $filename , string $mode [, bool $use_include_path = false [, resource $context ]])

# C语言
int open(const char *pathname, int flags);

会发现以上3种编程语言内置的打开文件的方法接收的参数中,除了都包含一个 "文件路径名称",还会包含一个 mode 参数(C语言的 open 函数中的 flags 参数作用相似)。这个 mode 参数定义的是打开文件时的模式,常见的文件打开模式有:只读、只写、可读可写、只追加。不同的编程语言中对文件打开模式的定义有些微小的差别,我们来看下 Python 中的文件打开模式有哪些。Python 中 open() 函数用于创建或打开指定文件,该函数的常用语法格式如下:

file_obj = open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, 
closefd=True, opener=None)

Python 的 open() 方法参数说明:

  1. file_obj:表示要创建的文件对象,类型可能是 <class '_io.TextIOWrapper'> 亦或者是 <class '_io.BufferedReader'>
  2. file:str or path-like object,要打开的文件路径,可以是绝对或相对路径,也可以是 Path 对象
  3. mode:str,可选参数,用于指定文件的打开模式。可选的打开模式如下方所示。如果不写,则默认以只读(r)模式打开文件。
    • 通用读写模式相关参数: 本组参数可以与文件格式参数组合使用,用于设置基本读、写操作权限,以及文件指针初始位置
      • 'r' 只读模式。默认。以只读方式打开一个文件,文件指针被定位到文件头的位置。如果该文件不存在,则会报错
      • 'w' 只写模式。打开一个文件只用于写入。如果该文件已存在,则打开文件,清空文件内容,并把文件指针定位到文件头位置开始编辑。如果该文件不存在,则创建新文件,打开并编辑
      • 'a' 追加模式。打开一个文件用于追加,仅有只写权限,无权读操作。如果该文件已存在,文件指针被定位到文件尾。新内容被写入到原内容之后。如果该文件不存在,则创建新文件并写入
    • 特殊读写模式相关参数:
      • '+' 更新模式。打开一个文件进行更新,具有可读、可写权限。注意,该模式不能单独使用, 需要与 r/w/a 模式组合使用。打开文件后,文件指针的位置由 r/w/a 组合模式决定
      • 'x' 只写模式。新建一个文件,打开并写入内容,如果该文件已存在,则会报错
    • 文件格式相关参数: 本组参数可以与其他模式参数组合使用,用于指定打开文件的格式,需要根据要打开文件的类型进行选择,此模式不能单独使用
      • b 二进制模式,以二进制格式打开文件,一般用于非文本文件,如图片、音视频等

      • t 文本模式,默认以文本格式打开文件,一般用于文本文件,通常省略,省略不影响功能,代码也更简洁。即在实际使用中,如果你没有特别说明模式是 'b'(即二进制),那么 Python 默认会使用 't' 模式(文本模式)。也就是说:'r' 就是 'rt' 的简写,两者完全等价,'w' 就是 'wt' 的简写,两者完全等价,包括后续的组合模式 'r+''rt+',也是完全一样的。反正你记住,模式中没有看到 'b' 出现,一律按文本格式处理。

        # 默认就是文本模式
        f1 = open("demo.txt", "r")   # 默认含 "t"
        f2 = open("demo.txt", "rt")  # 显式使用 "t"
        # 都是等价的
        
    • 组合模式: 文件格式与通用读写模式可以组合使用,另外通过组合+模式可以为只读、只写模式增加写、读的权限
      • 'r+' 文本格式读写。以文本格式打开一个文件用于读、写。文件指针被定位到文件头的位置。新写入的内容将覆盖掉原有文件部分或全部内容;如果该文件不存在,则会报错
      • 'rb' 二进制格式只读。以二进制格式打开一个文件,只能够读取。文件指针被定位到文件头的位置。一般用于非文本文件,如图片等
      • 'rb+' 二进制格式读写。以二进制格式打开一个文件用于读、写。文件指针被定位到文件头的位置。新写入的内容将覆盖掉原有文件部分或全部内容;如果该文件不存在,则会报错。一般用于非文本文件
      • 'w+' 文本格式写读。以文本格式打开一个文件用于写、读。如果该文件已存在,则打开文件,清空原有内容,进入编辑模式。如果该文件不存在,则创建新文件,打开并执行写、读操作
      • 'wb' 二进制格式只写。以二进制格式打开一个文件,只能够写入。如果该文件已存在,则打开文件,清空原有内容,进入编辑模式。如果该文件不存在,则创建新文件,打开并执行只写操作。一般用于非文本文件
      • 'wb+' 二进制格式写读。以二进制格式打开一个文件用于写、读。如果该文件已存在,则打开文件,清空原有内容,进入编辑模式。如果该文件不存在,则创建新文件,打开并执行写、读操作。一般用于非文本文件
      • 'a+' 文本格式读写。以文本格式打开一个文件用于读、写。如果该文件已存在,则打开文件,文件指针被定位到文件尾的位置,新写入的内容添加在原有内容的后面。如果该文件不存在,则创建新文件,打开并执行写、读操作
      • 'ab' 二进制格式只写。以二进制格式打开一个文件用于追加写入。如果该文件已存在,则打开文件,文件指针被定位到文件尾的位置,新写入的内容在原有内容的后面。如果该文件不存在,则创建新文件,打开并执行只写操作
      • 'ab+' 二进制格式读写。以二进制格式打开一个文件用于追加写入。如果该文件已存在,则打开文件,文件指针被定位到文件尾的位置,新写入的内容在原有内容的后面。如果该文件不存在,则创建新文件,打开并执行写、读操作
    • ps: 以二进制模式打开的文件(包含 'b'),返回文件内容为字节对象,而不进行任何解码。在文本模式(包含 't' 时,返回文件内容为字符串,已经解码。
  4. buffering:int,可选参数,用于指定对文件做读写操作时,是否使用缓冲区(本小节后续会详细介绍)
  5. encoding:str,手动设定打开文件时所使用的编码格式,不同平台的 ecoding 参数值也不同,以 Windows 为例,其默认为 cp936(可以理解为 GBK 编码),仅在文本模式下有效。建议,使用文件对象时,一定要指定编码,而不是使用默认编码
  6. errors:编码错误处理策略,例如 'ignore''replace''strict'
  7. newline:str,控制换行符的处理(None, '', '\n', '\r', '\r\n'
  8. closefd:bool,如果传入的是文件描述符(int),是否在关闭文件时也关闭该描述符(默认 True)
  9. opener:callable,可选的自定义打开函数,通常用于底层控制,如传递自定义 flags 给 os.open

文件打开模式,直接决定了后续可以对文件做哪些操作。例如,使用 r 模式打开的文件,后续编写的代码只能读取文件,而无法修改文件内容。 将以上几个容易混淆的文件打开模式的功能做了很好的对比:
在这里插入图片描述
【示例】默认打开 "base_demo.py" 文件。

# -*- coding: utf-8 -*-
# @Time    : 2025-05-12 5:36
# @Author  : AmoXiang
# @File    : 1.open打开文件.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

# 当前程序文件同目录下没有 base_demo.py 文件
file = open('base_demo.py')
print(file)

当以默认模式打开文件时,默认使用 r 权限,由于该权限要求打开的文件必须存在,因此运行此代码会报如下错误:
在这里插入图片描述
现在,在程序文件同目录下,打开存在的文件 test.py,并再次运行该程序,其运行结果为:

<_io.TextIOWrapper name='test.py' mode='r' encoding='cp936'>

可以看到,当前输出结果中,输出了 file 文件对象的相关信息,包括打开文件的名称、打开模式、打开文件时所使用的编码格式。使用 open() 打开文件时,默认采用 GBK 编码。但当要打开的文件不是 GBK 编码格式时,可以在使用 open() 函数时,手动指定打开文件的编码格式,例如:

file = open('test.py', encoding='utf-8')

注意,手动修改 encoding 参数的值,仅限于文件以文本的形式打开,也就是说,以二进制格式打开时,不能对 encoding 参数的值做任何修改,否则程序会抛出 ValueError 异常,如下所示:

# 执行该程序会报错
# ValueError: binary mode doesn't take an encoding argument
file = open('test.py', 'rb', encoding='utf-8')
print(file)

open() 是否需要缓冲区?(了解)

缓冲区(buffer)是内存中的一块临时存储空间,用于暂时保存要写入或读取的数据。通常情况下、建议大家在使用 open() 函数时打开缓冲区,即不需要修改 buffing 参数的值。如果 buffing 参数的值为 0(或者 False),则表示在打开指定文件时不使用缓冲区;buffering=1 ➜ 行缓冲(只对文本文件有效,每一行处理一次);如果 buffing 参数值为大于 1 的整数,该整数用于指定缓冲区的大小(单位是字节);如果 buffing 参数的值为负数,则代表Python 自动决定缓冲区的大小(推荐)。为什么呢?原因很简单,目前为止计算机内存的 I/O 速度仍远远高于计算机外设(例如键盘、鼠标、硬盘等)的 I/O 速度,如果不使用缓冲区,则程序在执行 I/O 操作时,内存和外设就必须进行同步读写操作,也就是说,内存必须等待外设输入(输出)一个字节之后,才能再次输出(输入)一个字节。这意味着,内存中的程序大部分时间都处于等待状态。而如果使用缓冲区,则程序在执行输出操作时,会先将所有数据都输出到缓冲区中,然后继续执行其它操作,缓冲区中的数据会有外设自行读取处理;同样,当程序执行输入操作时,会先等外设将数据读入缓冲区中,无需同外设做同步读写操作。大白话: 计算机中有两种设备:内存(Memory):非常快。外设(如硬盘、键盘等):比较慢。如果不使用缓冲区,每次读取或写入都要内存和外设“面对面”交流:
👩‍💻 内存:喂,硬盘,你准备好了吗?我要写一个字节了!
🧱 硬盘:(慢慢地)我准备好了……你可以写了。
这样一来,内存就只能等硬盘慢吞吞地读/写完一个字节,然后再处理下一个,程序运行会非常慢。使用缓冲区会怎样?
使用缓冲区后,内存就不需要等待硬盘每次都准备好:
写文件时:数据先写入内存中的缓冲区,等缓冲区满了或手动刷新,再一次性写入硬盘。
读文件时:会先从硬盘读取一大块数据到缓冲区中,然后程序从缓冲区里读取数据,访问更快。

open() 文件对象常用的属性: 成功打开文件之后,可以调用文件对象本身拥有的属性获取当前文件的部分信息,其常见的属性为:

属性/方法作用说明
file.closed✅ 判断文件是否已被关闭,返回 TrueFalse
file.mode✅ 返回打开文件时使用的模式,比如 'r''w''rb'
file.name✅ 返回文件的名称(字符串)
file.readable()✅ 判断文件是否可读,返回 TrueFalse
file.writable()✅ 判断文件是否可写,返回 TrueFalse
file.seekable()✅ 判断文件是否支持随机访问(即是否能使用 seek() 移动文件指针)

示例代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-12 5:36
# @Author  : AmoXiang
# @File    : 1.open打开文件.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680


f = open("test.py", "r", encoding="utf-8")

print("文件名:", f.name)
print("打开模式:", f.mode)
print("是否已关闭:", f.closed)
print("是否可读:", f.readable())  # True,因为 w+ 支持读写
print("是否可写:", f.writable())  # True
print("是否支持随机访问:", f.seekable())

f.close()
print("关闭后,是否已关闭:", f.closed)
# 运行结果如下: 
'''
文件名: test.py
打开模式: r
是否已关闭: False
是否可读: True
是否可写: False
是否支持随机访问: True
关闭后,是否已关闭: True
'''

3.3 以文本格式和二进制格式打开文件,到底有什么区别?

我们知道,open() 函数第二个参数是一个字符串,用于指定文件的打开方式,如果该字符串中出现 'b',则表示以二进制格式打开文件;反之,则以普通的文本格式打开文件。那么,文本文件和二进制文件有什么区别呢?

两种格式的解码区别: 根据我们以往的经验,文本文件通常用来保存肉眼可见的字符,比如 .txt 文件、.c 文件、.dat 文件等,用文本编辑器打开这些文件,我们能够顺利看懂文件的内容。而二进制文件通常用来保存视频、图片、音频等不可阅读的内容,当用文本编辑器打开这些文件,会看到一堆乱码,根本看不懂。实际上,从数据存储的角度上分析,二进制文件和文本文件没有区别,它们的内容都是以二进制的形式保存在磁盘中的。我们之所以能看懂文本文件的内容,是因为文本文件中采用的是 ASCII、UTF-8、GBK 等字符编码,文本编辑器可以识别出这些编码格式,并将编码值转换成字符展示出来。而对于二进制文件,文本编辑器无法识别这些文件的编码格式,只能按照字符编码格式胡乱解析,所以最终看到的是一堆乱码。

文本格式 open() 时的隐式转换: 使用 open() 函数以文本格式打开文件和以二进制格式打开文件,唯一的区别是对文件中换行符的处理不同。在 Windows 系统中,文件中用 "\r\n" 作为行末标识符(即换行符),当以文本格式读取文件时,会将 "\r\n" 转换成 "\n";反之,以文本格式将数据写入文件时,会将 "\n" 转换成 "\r\n"。这种隐式转换换行符的行为,对用文本格式打开文本文件是没有问题的,但如果用文本格式打开二进制文件,就有可能改变文本中的数据(将 "\r\n" 隐式转换为 "\n")。而在 Unix/Linux 系统中,默认的文件换行符就是 "\n",因此在 Unix/Linux 系统中文本格式和二进制格式并无本质的区别。总的来说,为了保险起见,对于 Windows 平台最好用 "b" 打开二进制文件;对于 Unix/Linux 平台,打开二进制文件,可以用 "b",也可以不用。

Python 的 open() 函数在底层,会依次使用以下几层对象来处理文件:

二进制模式: open() → FileIO → BufferedReader/BufferedWriter
# 文本模式: 文本模式是在二进制模式之上再套了一层专门处理"字符"的包装器 —— TextIOWrapper
open() → FileIO → BufferedReader/BufferedWriter → TextIOWrapper(处理字符编码)

文本模式和二进制模式的关键底层差异:

层面文本模式(Text Mode)二进制模式(Binary Mode)
数据结构返回 str(字符)返回 bytes(字节)
编码/解码自动执行(如 utf-8)无编码解码,原始字节
底层类TextIOWrapper 封装BufferedReaderBufferedWriter
I/O系统调用会在 TextIOWrapper 中进行解码,处理换行直接调用 read()write(),传输原始字节
换行符处理Windows 下 \r\n\n(自动转换)原样保留
seek()偏移量按字符偏移,可能因多字节字符而"不准确"按字节偏移,精确可靠
性能编解码开销更大性能更高、低延迟
跨平台行为会自动适配不同平台的换行符(\n\r\n无平台兼容处理,原始数据

示例代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 0:02
# @Author  : AmoXiang
# @File    : 2.t模式与b模式的区别.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

f1 = open("1.txt", "r", encoding="utf-8")
'''
你会发现 f 实际是 TextIOWrapper 对象。它负责: 
    ① 自动从底层的 BufferedReader 获取原始字节流
    ② 按指定编码(utf-8)解码为 str
    ③ 替你处理换行符转换
'''
print(type(f1))  # <class '_io.TextIOWrapper'>
# 而如果你打开文件时使用 'rb'

f2 = open("1.txt", "rb")
# 这时 f 就是字节级别的对象,编码解码操作完全交给你自己
print(type(f2))  # <class '_io.BufferedReader'>

# 拓展:
# 1.字节(bytes) 是数据的最小单位,CPU、硬盘、网络都以字节为单位进行通信
# 2.字符(str) 是人类语义单位,它需要依赖编码(如 UTF-8、GBK、ASCII)来表示成字节
# 所以: 
# 1.文本模式是: Python 把"看得懂"的字符编码成字节再传给底层系统
# 2.二进制模式是: Python 直接操作字节,尽可能贴近硬件

3.4 Python read()方法:按字节(字符)读取文件

Python 提供了如下 3 种方法,它们都可以帮我们实现读取文件中数据的操作:

  1. read() 方法:逐个字节或者字符读取文件中的内容
  2. readline() 方法:逐行读取文件中的内容
  3. readlines() 方法:一次性读取文件中多行内容

本小节先讲解 read() 方法的用法,readline() 和 readlines() 方法会放到 3.5 小节 中作详细介绍。对于借助 open() 函数,并以可读模式(包括 r、r+、rb、rb+)打开的文件,可以调用 read() 函数逐个字节(或者逐个字符)读取文件中的内容。

如果文件是以文本模式(非二进制模式)打开的,则 read() 函数会逐个字符进行读取;反之,如果文件以二进制模式打开,则 read() 函数会逐个字节进行读取

read() 函数的基本语法格式如下:

In [2]: f.read?
Signature: f.read(size=-1, /)
Docstring:
Read at most size characters from stream.

Read from underlying buffer until we have size characters or we hit EOF.
If size is negative or omitted, read until EOF.
Type:      builtin_function_or_method

其中,f 表示已打开的文件对象;size 作为一个可选参数,用于指定一次最多可读取的字符(字节)个数,如果省略,则默认一次性读取
所有内容。

举个例子,首先创建一个名为 test.txt 的文本文件,其内容为:

100天精通Python
https://blog.youkuaiyun.com/xw1680/category_12955592.html

然后在和 test.txt 同目录下,创建一个 read_demo.py 文件,并编写如下语句:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 0:18
# @Author  : AmoXiang
# @File    : 3.read_demo.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

# #以 utf-8 的编码格式打开指定文件
f = open('test.txt', 'r', encoding='utf-8')
# 输出读取到的数据
print(f.read())
# 关闭文件
f.close()

程序执行结果为:
在这里插入图片描述
当然,我们也可以通过使用 size 参数,指定 read() 每次可读取的最大字符(或者字节)数,例如:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 0:18
# @Author  : AmoXiang
# @File    : 3.read_demo.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

# #以 utf-8 的编码格式打开指定文件
f = open('test.txt', 'r', encoding='utf-8')
# 输出读取到的数据 结果为: 100天精通
print(f.read(6))
# 关闭文件
f.close()

显然,该程序中的 read() 函数只读取了 test.txt 文件开头的 6 个字符。再次强调,size 表示的是一次最多可读取的字符(或字节)数,因此,即便设置的 size 大于文件中存储的字符(字节)数,read() 函数也不会报错,它只会读取文件中所有的数据。除此之外,对于以二进制格式打开的文件,read() 函数会逐个字节读取文件中的内容。例如:

# 以二进制形式打开指定文件
f = open("test.txt", 'rb+')
# 输出读取到的数据
# b'100\xe5\xa4\xa9\xe7\xb2\xbe\xe9\x80\x9aPython\r\nhttps://blog.youkuaiyun.com/xw1680/category_12955592.html'
print(f.read())
# 关闭文件
f.close()

可以看到,输出的数据为 bytes 字节串。我们可以调用 decode() 方法,将其转换成我们认识的字符串。有关 bytes 字节串,读者可阅读 《100天精通Python——基础篇 2025 第7天:字符串操作全掌握与编码基础剖析》中的 四、编码与解码 小节做详细了解。另外需要注意的一点是,想使用 read() 函数成功读取文件内容,除了严格遵守 read() 的语法外,其还要求 open() 函数必须以可读默认(包括 r、r+、rb、rb+)打开文件。举个例子,将上面程序中 open() 的打开模式改为 w,程序会抛出 io.UnsupportedOperation 异常,提示文件没有读取权限:
在这里插入图片描述
read() 函数抛出 UnicodeDecodeError 异常的解决方法: 在使用 read() 函数时,如果 Python 解释器提示 UnicodeDecodeError 异常,其原因在于,目标文件使用的编码格式和 open() 函数打开该文件时使用的编码格式不匹配。举个例子,如果目标文件的编码格式为 GBK 编码,而我们在使用 open() 函数并以文本模式打开该文件时,手动指定 encoding 参数为 UTF-8。这种情况下,由于编码格式不匹配,当我们使用 read() 函数读取目标文件中的数据时,Python 解释器就会提示 UnicodeDecodeError 异常。要解决这个问题,要么将 open() 函数中的 encoding 参数值修改为和目标文件相同的编码格式,要么重新生成目标文件(即将该文件的编码格式改为和 open() 函数中的 encoding 参数相同)。除此之外,还有一种方法:先使用二进制模式读取文件,然后调用 bytes 的 decode() 方法,使用目标文件的编码格式,将读取到的字节串转换成认识的字符串。举个例子:

# 在windows下使用记事本工具,输入以下内容,并以ANSI编码保存到桌面,命名为test.txt
hello,世间种种的诱惑
# 在 Windows 上,"ANSI" 不是一个真正的编码标准,而是一个 模糊的本地编码概念,它依赖于系统的默认区域设置(语言环境)
# 如果你的 Windows 设置为简体中文,ANSI 实际等于 GBK(兼容 GB2312)

# 以二进制形式打开指定文件
f = open(r"C:\Users\amoxiang\Desktop\test.txt", 'r', encoding='utf-8')
# 输出读取到的数据
# 报错: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 5: invalid start byte
print(f.read())
# 关闭文件
f.close()

# 正确方式1: 以二进制方式读取,再用 GBK 解码
with open(r"C:\Users\amoxiang\Desktop\test.txt", 'rb') as f:
    byte_data = f.read()  # 读取为字节串

# 使用 GBK 编码进行解码
text = byte_data.decode('gbk')
print(text)  # 正确输出: hello,世间种种的诱惑

# 正确方式2: 先检测文件的编码格式,以便你正确选择open()函数中的 encoding 参数
# 使用 chardet 库(推荐): pip install chardet
# ps:  只是猜测编码,不是 100% 准确,但对于中文文本文件(GBK、UTF-8、UTF-16)通常很可靠
import chardet

# 读取部分字节内容用于判断编码(通常不需要读取整个文件)
with open(r"C:\Users\amoxiang\Desktop\test.txt", 'rb') as f:
    raw_data = f.read(1000)  # 读取前 1000 个字节

result = chardet.detect(raw_data)
print(result)
# {'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}
# 以二进制形式打开指定文件
f = open(r"C:\Users\amoxiang\Desktop\test.txt", 'r', encoding=result['encoding'])
# 输出读取到的数据
print(f.read())
# 关闭文件
f.close()

输出内容乱码与 UnicodeDecodeError 异常的解决方法是一样的,都是编解码格式不统一导致的。总结三种解决方案:

方案描述
✅ 推荐 1修改 open()encoding 参数,使其与文件编码一致(如 'gbk'
✅ 推荐 2转换文件编码(用文本编辑器或 iconv 工具改为 utf-8
✅ 推荐 3rb 读取为字节,再用 .decode(编码格式) 方法手动转换

在给个示例:

filename = './test20250513-3.txt'
f = open(filename, 'w+', encoding='utf-8')
f.write('Amo教育')
f.close()
f = open(filename, encoding='utf-8')
print(1, f.read(1))  # 按字符 ==> 1 A
print(2, f.read(2))  # 2 mo
print(3, f.read())  # 3 教育
f.close()
f = open(filename, 'rb')
print(4, f.read(1))  # 按字节 4 b'A'
print(5, f.read(2))  # 5 b'mo'
# gbk编码: 两个字节表示一个字符
print(6, 'Amo教育'.encode('gbk'))  # b'Amo\xbd\xcc\xd3\xfd'
# utf-8编码: 三个字节表示一个字符
print(7, f.read())  # 7 b'\xe6\x95\x99\xe8\x82\xb2'
f.close()

3.5 Python readline()和readlines()方法:按行读取文件

3.4 小节 中讲到,如果想读取用 open() 函数打开的文件中的内容,除了可以使用 read() 方法,还可以使用 readline() 和 readlines() 方法。和 read() 方法不同,这 2 个函数都以 "行" 作为读取单位,即每次都读取目标文件中的一行。对于读取以文本格式打开的文件,读取一行很好理解;对于读取以二进制格式打开的文件,它们会以 "\n" 作为读取一行的标志。

readline() 方法用于读取文件中的一行,包含最后的换行符 "\n"。此函数的基本语法格式为:

In [4]: f.readline?
Signature: f.readline(size=-1, /)
Docstring:
Read until newline or EOF.

Return an empty string if EOF is hit immediately.
If size is specified, at most size characters will be read.
Type:      builtin_function_or_method

其中,f 为打开的文件对象;size 为可选参数,用于指定读取每一行时,一次最多读取的字符(字节)数。和 read() 函数一样,此函数成功读取文件数据的前提是,使用 open() 函数指定打开文件的模式必须为可读模式(包括 r、rb、r+、rb+ 4 种)。仍以 3.4 小节 中创建的 test.txt 文件为例,该文件中有如下 2 行数据:

100天精通Python
https://blog.youkuaiyun.com/xw1680/category_12955592.html

示例:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 8:40
# @Author  : AmoXiang
# @File    : 4.readline()与readlines()方法的使用.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

f = open('test.txt', encoding='utf-8')
# 读取一行数据
row = f.readline()
print(row)

程序执行结果为:
在这里插入图片描述
由于 readline() 函数在读取文件中一行的内容时,会读取最后的换行符 "\n",再加上 print() 函数输出内容时默认会换行,所以输出结果中会看到多出了一个空行。不仅如此,在逐行读取时,还可以限制最多可以读取的字符(字节)数,例如:

f = open('test.txt', 'rb')
# 读取一行数据
row = f.readline(6)
print(row)  # b'100\xe5\xa4\xa9'
print(row.decode('utf-8'))  # 100天

运行结果为:
在这里插入图片描述
和上一个例子的输出结果相比,由于这里没有完整读取一行的数据,因此不会读取到换行符。

readlines() 方法用于读取文件中的所有行,它和调用不指定 size 参数的 read() 方法类似,只不过该函数返回是一个字符串列表,其中每个元素为文件中的一行内容。和 readline() 方法一样,readlines() 方法在读取每一行时,会连同行尾的换行符一块读取。readlines() 方法的基本语法格式如下:

In [5]: f.readlines?
Signature: f.readlines(hint=-1, /)
Docstring:
Return a list of lines from the stream.

hint can be specified to control the number of lines read: no more
lines will be read if the total size (in bytes/characters) of all
lines so far exceeds hint.
Type:      builtin_function_or_method

其中,f 为打开的文件对象。和 read()、readline() 函数一样,它要求打开文件的模式必须为可读模式(包括 r、rb、r+、rb+ 4 种)。示例:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 8:40
# @Author  : AmoXiang
# @File    : 4.readline()与readlines()方法的使用.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

f = open('test.txt', encoding='utf-8')
rows = f.readlines()
'''
['100天精通Python\n', 'https://blog.youkuaiyun.com/xw1680/category_12955592.html']
'''
print(rows)
f.close()

3.6 Python write()和writelines():向文件中写入数据

Python 中的文件对象提供了 write() 方法,可以向文件中写入指定内容。该方法的语法格式如下:

In [6]: f.write?
Signature: f.write(text, /)
Docstring:
Write string s to stream.

Return the number of characters written
(which is always equal to the length of the string).
Type:      builtin_function_or_method

其中,f 表示已经打开的文件对象;text 表示要写入文件的字符串(或字节串,仅适用写入二进制文件中)。注意,在使用 write() 向文件中写入数据,需保证使用 open() 函数是以 r+、w、w+、a 或 a+ 的模式打开文件,否则执行 write() 函数会抛出 io.UnsupportedOperation 错误。例如,创建一个 write_demo.txt 文件,该文件内容如下:

100天精通Python
https://blog.youkuaiyun.com/xw1680/category_12955592.html

然后,在和 write_demo.txt 文件同级目录下,创建一个 Python 文件,编写如下代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 7:58
# @Author  : AmoXiang
# @File    : 4.write_demo.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

f = open('write_demo.txt', 'w', encoding='utf-8')
f.write('你好,write()方法!!!')
f.close()

前面已经讲过,如果打开文件模式中包含 w(写入),那么向文件中写入内容时,会先清空原文件中的内容,然后再写入新的内容。因此运行上面程序,再次打开 write_demo.txt 文件,只会看到新写入的内容:
在这里插入图片描述
而如果打开文件模式中包含 a(追加),则不会清空原有内容,而是将新写入的内容会添加到原内容后边。例如,还原 write_demo.txt 文件中的内容,并修改上面代码为:

f = open('write_demo.txt', 'a', encoding='utf-8')
f.write('\n你好,write()方法!!!')
f.close()

再次打开 write_demo.txt,可以看到如下内容:
在这里插入图片描述
因此,采用不同的文件打开模式,会直接影响 write() 函数向文件中写入数据的效果。另外,在写入文件完成后,一定要调用 close() 函数将打开的文件关闭,否则写入的内容不会保存到文件中。此处演示使用 Pycharm 看不出来效果,打开 cmd 窗口,输入下面的代码:

C:\Users\amoxiang\Desktop>python3

C:\Users\amoxiang\Desktop>python
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> f = open('test.txt', 'w', encoding='utf-8')
>>> f.write('我是一只小小鸟')
7

你会发现该 test.txt 文件是空的。这是因为,当我们在写入文件内容时,操作系统不会立刻把数据写入磁盘,而是先缓存起来,只有调用 close() 函数时,操作系统才会保证把没有写入的数据全部写入磁盘文件中。除此之外,如果向文件写入数据后,不想马上关闭文件,也可以调用文件对象提供的 flush() 函数,它可以实现将缓冲区的数据写入文件中。例如:

f = open('write_demo.txt', 'w', encoding='utf-8')
f.write('写入一行新数据')
# 打开 write_demo.txt 文件,可以看到写入的新内容: 写入一行新数据
f.flush()

拓展: 在 PyCharm 中没调用 close() 或 flush(),写入数据依然成功,很可能是因为:

  1. 数据量小:缓冲区还没来得及 "拖延写入"。操作系统的文件写入有缓冲机制,但如果写入内容很小(比如几十个字节),操作系统有时会直接写入磁盘(或者缓冲区刚好满了,也会自动 flush)。所以即使你没手动调用 close() 或 flush(),在小文件写入中 "看起来" 没问题。
  2. Python 解释器进程退出时会自动调用 close()。如果你用的是 PyCharm 或正常运行 Python 脚本,程序运行结束时:Python 的 GC(垃圾回收器)会销毁 f 对象;此时会自动调用 f.__del__() → 最终调用 f.close();所以缓冲区被自动刷新,文件成功写入磁盘。⚠️ 但这并不是可靠行为!Python 的垃圾回收时机在复杂场景下并不保证立即执行(例如多线程、异常中断、解释器崩溃等)。
  3. PyCharm 的运行环境可能会 "保护性地清理资源"。PyCharm 是智能 IDE,它运行 Python 时可能添加了保护钩子,确保你运行脚本结束后文件被关闭(不代表普通 Python 环境也这样做)。

所以在 PyCharm 中,"看上去一切都正常",但你不能依赖它的行为在其他环境中也成立。总结:你观察到的写入成功现象属于 "侥幸" 行为。

条件是否安全可靠?原因
写数据少,程序正常结束✅ 一般会写成功OS 可能自动 flush,或 Python 退出自动 close
写数据多,程序中途崩溃❌ 有可能丢失数据缓冲区没 flush,文件可能为空
忘记 close 而直接依赖自动机制❌ 非推荐不稳定,不可控,容易踩坑

有人可能会想到,通过设置 open() 函数的 buffering 参数可以关闭缓冲区,这样数据不就可以直接写入文件中了?对于以二进制格式打开的文件,可以不使用缓冲区,写入的数据会直接进入磁盘文件;但对于以文本格式打开的文件,必须使用缓冲区,否则 Python 解释器会 ValueError 错误。例如:

f = open('write_demo.txt', 'w', encoding='utf-8', buffering=0)
# ValueError: can't have unbuffered text I/O
f.write('写入一行新数据')

Python 的文件对象中,不仅提供了 write() 方法,还提供了 writelines() 方法,可以实现将字符串列表写入文件中。注意,写入方法只有 write() 和 writelines() 方法,而没有名为 writeline 的方法。例如,还是以 write_demo.txt 文件为例,通过使用 writelines() 方法,可以轻松实现将 write_demo.txt 文件中的数据复制到其它文件中,实现代码如下:

f1 = open('write_demo.txt', 'r', encoding='utf-8')
f2 = open('write_demo_bak.txt', 'w+', encoding='utf-8')
f2.writelines(f1.readlines())
f2.close()
f1.close()

执行此代码,在 write_demo.txt 文件同级目录下会生成一个 write_demo_bak.txt 文件,且该文件中包含的数据和 write_demo.txt 完全一样。需要注意的是,使用 writelines() 函数向文件中写入多行数据时,不会自动给各行添加换行符。上面例子中,之所以 write_demo_bak.txt 文件中会逐行显示数据,是因为 readlines() 函数在读取各行数据时,读入了行尾的换行符。

3.7 Python close()函数:关闭文件

在前面的小节中,对于使用 open() 函数打开的文件,我们一直都在用 close() 方法将其手动关闭。本小节就来详细介绍一下 close() 方法。close() 方法是专门用来关闭已打开文件的,其语法格式也很简单,如下所示:

In [2]: f.close?
Signature: f.close()
Docstring:
Flush and close the IO object.

This method has no effect if the file is already closed.
Type:      builtin_function_or_method

其中,f 表示已打开的文件对象。读者可能一直存在这样的疑问,即使用 open() 函数打开的文件,在操作完成之后,一定要调用 close() 函数将其关闭吗?答案是肯定的。文件在打开并操作完成之后,就应该及时关闭,否则程序的运行可能出现问题。举个例子,分析如下代码:

C:\Users\amoxiang\Desktop>ipython
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.32.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: f = open('test.txt', 'w', encoding='utf-8')
# .....
In [3]: import os
In [4]: os.remove('test.txt')
---------------------------------------------------------------------------
PermissionError                           Traceback (most recent call last)
Cell In[4], line 1
----> 1 os.remove('test.txt')

PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。: 'test.txt'

代码中,我们引入了 os 模块,调用了该模块中的 remove() 函数,该函数的功能是删除指定的文件。发现运行此程序,Python 解释器报错。显然,由于我们使用了 open() 函数打开了 test.txt 文件,但没有及时关闭,直接导致后续的 remove() 函数运行出现错误。因此,正确的程序应该是这样的:

In [1]: f = open('test.txt', 'w', encoding='utf-8')
In [3]: import os
In [5]: f.close()
# 发现解释器不在报错,test.txt文件从桌面上被删除掉
In [6]: os.remove('test.txt')

关于写入的问题在 《3.6 Python write()和writelines():向文件中写入数据》 小节中已经演示过,这里不在赘述。关于文件描述符,资源释放的问题,也在 《二、文件读写实现原理与操作步骤》 中进行了讲解,这里也不再进行赘述。

3.8 Python seek()和tell()方法详解

在讲解 seek() 方法和 tell() 方法之前,首先来了解一下什么是文件指针。我们知道,使用 open() 函数打开文件并读取文件中的内容时,总是会从文件的第一个字符(字节)开始读起。那么,有没有办法可以自定指定读取的起始位置呢?答案是肯定,这就需要移动文件指针的位置。

文件指针用于标明文件读写的起始位置。假如把文件看成一个水流,文件中每个数据(以 b 模式打开,每个数据就是一个字节;以普通模式打开,每个数据就是一个字符)就相当于一个水滴,而文件指针就标明了文件将要从文件的哪个位置开始读起。下图简单示意了文件指针的概念:
在这里插入图片描述
可以看到,通过移动文件指针的位置,再借助 read() 和 write() 方法,就可以轻松实现,读取文件中指定位置的数据(或者向文件中的指定位置写入数据)。注意,当向文件中写入数据时,如果不是文件的尾部,写入位置的原有数据不会自行向后移动,新写入的数据会将文件中处于该位置的数据直接覆盖掉。实现对文件指针的移动,文件对象提供了 tell() 方法和 seek() 方法。tell() 方法用于判断文件指针当前所处的位置,而 seek() 方法用于移动文件指针到文件的指定位置。tell() 方法的用法很简单,其基本语法格式如下:

In [11]: f.tell?
Signature: f.tell()
Docstring:
Return the stream position as an opaque number.

The return value of tell() can be given as input to seek(), to restore a
previous stream position.
Type:      builtin_function_or_method

其中,f 表示文件对象。例如,在同一目录下,编写如下程序对 a.txt 文件做读取操作,a.txt 文件中内容为:

https://blog.csdn.net/xw1680/category_12955592.html

读取 a.txt 的代码如下:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 10:38
# @Author  : AmoXiang
# @File    : 5.tell()方法.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

f = open('a.txt', 'r', encoding='utf-8')
print(f.tell())
print(f.read(3))
print(f.tell())
'''
0
htt
3
'''

可以看到,当使用 open() 函数打开文件时,文件指针的起始位置为 0,表示位于文件的开头处,当使用 read() 方法从文件中读取 3 个字符之后,文件指针同时向后移动了 3 个字符的位置。这就表明,当程序使用文件对象读写数据时,文件指针会自动向后移动:读写了多少个数据,文件指针就自动向后移动多少个位置。seek() 方法用于将文件指针移动至指定位置,该函数的语法格式如下:

@abstractmethod
def seek(self, offset: int, whence: int = 0) -> int:
    pass

f.seek()

其中,各个参数的含义如下:

  1. f:表示文件对象
  2. whence:作为可选参数,用于指定文件指针要放置的位置,该参数的参数值有 3 个选择:0 代表文件头(默认值)、1 代表当前位置、2 代表文件尾。
  3. offset:表示相对于 whence 位置文件指针的偏移量,正数表示向后偏移,负数表示向前偏移。例如,当 whence == 0 &&offset == 3(即 seek(3,0) ),表示文件指针移动至距离文件开头处 3 个字符的位置;当 whence == 1 &&offset == 5(即 seek(5,1) ),表示文件指针向后移动,移动至距离当前位置 5 个字符处。
  4. 注意,当 offset 值非 0 时,Python 要求文件必须要以二进制格式打开,否则会抛出 io.UnsupportedOperation 错误。

示例:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 10:47
# @Author  : AmoXiang
# @File    : 6.seek()方法.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680


f = open('a.txt', 'rb')

'''
a.txt内容: 
https://blog.youkuaiyun.com/xw1680/category_12955592.html
'''

# 判断文件指针的位置
print(f.tell())  # 0
# 读取一个字节,文件指针自动后移1个数据
print(f.read(1))  # b'h'
print(f.tell())  # 1
# 将文件指针从文件开头,向后移动到 5 个字符的位置
f.seek(5)
print(f.tell())  # 5
print(f.read(1))  # b':'
print(f.tell())  # 6
# 将文件指针从当前位置,向后移动到 5 个字符的位置
f.seek(5, 1)
print(f.tell())  # 11
print(f.read(1))  # b'g'
# 将文件指针从文件结尾,向前移动到距离 2 个字符的位置
f.seek(-1, 2)  # 
print(f.tell())  # 50
print(f.read(1))  # b'l'

注意:由于程序中使用 seek() 时,使用了非 0 的偏移量,因此文件的打开方式中必须包含 b,否则就会报 io.UnsupportedOperation: can't do nonzero end-relative seeks 错误,有兴趣可以自己测试。上面程序示范了使用 seek() 方法来移动文件指针,包括从文件开头、指针当前位置、文件结尾处开始计算。运行上面程序,结合程序输出结果可以体会文件指针移动的效果。由于文章篇幅有限,下面感兴趣的你们也可以自行去测试:

  1. 文本模式
    • seek(0)、seek(200),相对开始向右偏移,不能左超界
    • seek(0, 1),只能是偏移 0,就是原地踏步,没有用的操作
    • seek(0, 2),只能是偏移 0,就是跳到 EOF
  2. 字节模式
    • seek(0)、seek(200),相对开始向右偏移,不能左超界
    • seek(0, 1)、seek(-5, 1)、seek(5, 1),相对当前索引位置偏移,不能左超界
    • seek(0, 2)、seek(-5, 2)、seek(5, 2),相对 EOF 偏移,不能左超界
  3. 最常用的操作就是 seek(0)、seek(0, 2)

3.9 上下文管理

文件对象这种打开资源并一定要关闭的对象,为了保证其打开后一定关闭,为其提供了上下文支持。语法格式:

with 文件对象 as 标识符: # 等同于 标识符 = 文件对象
	pass # 标识符可以在内部使用

上下文管理:

  1. 使用 with 关键字,上下文管理针对的是 with 后的对象
  2. 使用 with … as 关键字
  3. 上下文管理的语句块并不会开启新的作用域

文件对象上下文管理:

  1. 进入 with 时,with 后的文件对象是被管理对象
  2. as 子句后的标识符,指向 with 后的文件对象
  3. with 语句块执行完的时候,会自动关闭文件对象

示例代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 11:03
# @Author  : AmoXiang
# @File    : 7.上下文管理.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680


file_name = './a.txt'
with open(file_name, 'r') as f:
    print(1, f.closed)
    print(f.write('abcd'))  # r模式写入失败,抛异常

print(2, f.closed)  # with中不管是否抛异常,with结束时都会保证关闭文件对象

写入示例:

C:\Users\amoxiang\Desktop>python
Python 3.12.8 (tags/v3.12.8:2dc476b, Dec  3 2024, 19:30:04) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
# 发现并未显示调用close()方法,但是桌面文件1.txt写入内容成功,可见with...as语句已经自动帮我们进行了处理
>>> with open('1.txt', 'w', encoding='utf-8') as file:
...     file.write('hello, Amo!我是一只小小鸟~~~')
...
21

with as 语句实现的底层原理到底是怎样的呢?可以阅读 《100天精通Python——基础篇 2025 第14天:深入掌握魔术方法与元类,玩转高级OOP技巧》一文的 《八、魔术方法之上下文管理》 小节做详细了解。

3.10 文件的遍历

类似于日志文件,文件需要遍历,最常用的方式就是逐行遍历。示例代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 11:16
# @Author  : AmoXiang
# @File    : 8.文件的遍历.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

filename = './b.txt'
with open(filename, 'w', encoding='utf-8') as f:
    f.write('\n'.join(map(str, range(101, 120))))

with open(filename, 'r', encoding='utf-8') as f:
    for line in f:  # 文件对象时可迭代对象,逐行遍历
        print(line.encode())  # 带换行符

3.11 路径操作

  1. 《Python 常用模块(三):os.path模块》 👉 查看文章

四、shutil模块

《Python 常用模块(四):shutil模块》 👉 查看文章

五、序列化和反序列化

5.1 为什么要序列化?

内存中的字典、列表、集合以及各种对象,如何保存到一个文件中?如果是自己定义的类的实例,如何保存到一个文件中?如何从文件中读取数据,并让它们在内存中再次恢复成自己对应的类的实例?要设计一套协议,按照某种规则,把内存中数据保存到文件中。文件是一个字节序列,所以必须把数据转换成字节序列,输出到文件。这就是序列化。 反之,从文件的字节序列恢复到内存并且还是原来的类型,就是反序列化。

5.2 定义

serialization 序列化:将内存中对象存储下来,把它变成一个个字节。 数据结构 → 二进制
deserialization 反序列化:将文件的一个个字节恢复成内存中对象。 二进制 → 数据结构

序列化保存到文件就是持久化。可以将数据序列化后持久化,或者网络传输;也可以将从文件中或者网络接收到的字节序列反序列化。Python 提供了 pickle 库。

5.3 pickle

Python 中的序列化、反序列化模块。

函数说明
dumps对象序列化为bytes对象
dump对象序列化到文件对象,就是存入文件
loads从bytes对象反序列化
load对象反序列化,从文件读取数据

示例代码:

# -*- coding: utf-8 -*-
# @Time    : 2025-05-13 11:38
# @Author  : AmoXiang
# @File    : 9.pickle_demo.py
# @Software: PyCharm
# @Blog: https://blog.youkuaiyun.com/xw1680

import pickle

filename = './ser'
# 序列化后看到什么
i = 99
c = 'c'
l = list('123')
d = {'a': 127, 'b': 'abc', 'c': [1, 2, 3]}
# 序列化
with open(filename, 'wb') as f:
    pickle.dump(i, f)
    pickle.dump(c, f)
    pickle.dump(l, f)
    pickle.dump(d, f)

# 反序列化
with open(filename, 'rb') as f:
    print(f.read(), f.seek(0))
    for i in range(4):
        x = pickle.load(f)
        print(i, x, type(x))

5.4 序列化应用

一般来说,本地序列化的情况,应用较少。大多数场景都应用在网络传输中。将数据序列化后通过网络传输到远程节点,远程服务器上的服务将接收到的数据反序列化后,就可以使用了。但是,要注意一点,远程接收端,反序列化时必须有对应的数据类型,否则就会报错。尤其是自定义类,必须远程得有一致的定义。现在,大多数项目,都不是单机的,也不是单服务的,需要多个程序之间配合。需要通过网络将数据传送到其他节点上去,这就需要大量的序列化、反序列化过程。但是,问题是,Python 程序之间可以都用 pickle 解决序列化、反序列化,如果是跨平台、跨语言、跨协议 pickle 就不太适合了,就需要公共的协议。例如 XML、Json、Protocol Buffer、msgpack 等。不同的协议,效率不同、学习曲线不同,适用不同场景,要根据不同的情况分析选型。

5.5 JSON

《Python 常用模块(二):json模块》 👉 查看文章

六、csv文件

《Python 常用模块(一):csv模块》 👉 查看文章

👉 下一天的学习: 点击阅读 《100天精通Python——基础篇 2025 第20天:正则表达式入门实战,解锁字符串处理的魔法力量》

至此今天的学习就到此结束了,本文为个人学习记录与复习整理之用,旨在帮助自己系统巩固Python相关知识,同时也希望能为正在学习该领域的同学提供一些参考与帮助。部分内容参考了公开课资料、他人学习笔记或网络公开资源,其中部分图片或示意图来自网络,仅用于非商业性质的学习交流。如有侵权或不当引用之处,敬请联系我删除或更正。感谢您阅读本篇博文,希望本文能成为您编程路上的领航者。祝您阅读愉快!
在这里插入图片描述

    好书不厌读百回,熟读课思子自知。而我想要成为全场最靓的仔,就必须坚持通过学习来获取更多知识,用知识改变命运,用博客见证成长,用行动证明我在努力。
    如果我的博客对你有帮助、如果你喜欢我的博客内容,请 点赞评论收藏 一键三连哦!听说点赞的人运气不会太差,每一天都会元气满满呦!如果实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
 编码不易,大家的支持就是我坚持下去的动力。点赞后不要忘了 关注 我哦!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

棒棒编程修炼场

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值