- Python程序设计
- 张雪萍主编
- 7029字
- 2020-08-27 23:59:44
3.1 读写文件
3.1.1 文件对象声明与基本操作
与其他语言处理文件类似,Python 文件操作的基本流程是:打开文件→对文件进行读、写或其他操作→关闭文件。
1.文件的打开
使用内置函数open来打开文件,语法格式如下:
它可以接受3个参数,第一个是路径(必不可少),第二个是操作的模式(可以指定它是读或写,或者其他的操作),第三个是编码,它可以指定当前文件读取或写入的字符串编码是什么。后面两个参数是可以省略的。
1)路径的写法
【例 3-1】 假设在 C 盘的 path 路径下有一个文本文件“data.txt”,则有两种表达该路径的方法。
方法一:'c:\\path\\data.txt',利用转义字符指定路径。由于“\”需要转义,所以这里要用两个“\\”。
方法二:r'c:\path\data.txt',利用“r”将路径声明为原始字符串。
以上两种方式指定的都是绝对路径,即绝对地址,而在某些情况下可以直接写一个文件名('data.txt'),即前面不指定完整的路径,以相对路径的方式来表达。
在采用相对路径表示时,系统会在当前的系统环境变量下去找有没有同名的文件。不过建议大家尽量采用绝对路径的表达方式,因为绝对路径写得更清晰。
有时我们在写代码时,希望省略前面的磁盘目录名称,直接写一个文件名,这时可以通过加载 os 模块来改变当前目录[2]。Python 标准库中的 os 模块包含普遍的操作系统功能。如果希望程序与平台无关的话,这个模块是尤为重要的。os模块提供了非常丰富的方法来处理文件和目录。看下面的代码:
以上异常信息显示在当前目录下没有找到要打开的文件,那么当前的目录是什么?可以通过导入模块os来解决这个问题。使用os模块中的方法“getcwd()”可以获得当前的操作目录。
浏览此目录发现的确没有“data.txt”文件。
通过 os 模块中的方法“chdir()”可以进行目录切换操作,这样在打开或写文件时就可以省略文件所在的目录路径,只写一个文件名称即可。
os 模块提供了很多对文件目录进行操作的方法,如判断指定目录下的指定文件是否存在、获取指定目录下的所有文件或子文件夹里的所有文件、删除指定目录下的文件、创建新目录,还可以创建多级目录等。os模块的常用方法如表3-1所示。
可以在导入os模块后,通过help(os)命令查阅更多的方法及它们的语法格式。
表3-1 os模块的常用方法
注意:当父目录不存在时,os.mkdir(path)不会创建目录,但是 os.makedirs(path)会创建父目录。
【例 3-2】 在 D 盘创建目录“D:\456\123”。说明,D 盘事先不存在文件夹“456”。
在D盘没有文件夹“456”的情况下,利用方法makedirs可以在“456”文件夹下创建下级子文件夹“123”。
说明os.makedirs()创建文件夹成功。
【例3-3】 将当前目录改为“D:\456\123”,然后再返回上一级目录。
2)模式
模式用来指定打开文件时的操作方式。在 Python3 中,文本文件被当作“Unicode”字符串对待,二进制文件中的内容则以字节的形式来操作。
利用全局函数 open 声明文件对象时,在"模式"这个位置指定一个字符串来表示文件的打开模式。文件打开的主要模式如表3-2所示。
表3-2 文件打开的主要模式
使用 open 函数打开文件时,如果"模式"处是'r',则表示以读的方式来操作当前的文件。如果要写文件的话,把'r'换成'w'。如果想同时对这个文件进行读、写操作,可以用'rw'。如果想在原有文件的基础上追加一些内容,则可以用'a'。
二进制文件是以字节形式来操作的。其在模式指定上有些差别,需要在字母 r、w、a后面加上“b”,把它声明为二进制字节的方式。
本章只讨论对文本文件进行读、写等操作。
通过任意记事本在 D 盘的“D:\python\PythonEXample”目录下创建一个文本文件“data.txt”,在保存文件时请注意编码的选择,如图3-1所示。
图3-1 保存文件时编码的选择
在Windows下的编码是“ANSI”,它的字符是以“gbk”的形式保存的。如果想让文本文件的兼容性更高,可以选择“UTF-8”。目前默认是“ANSI”。
接下来就可以用Python来对这个文本文件进行操作了。
2.声明文件
前面声明其他变量时比较简单,比如声明一个 int 型变量“i”,或者声明字符串变量“s”:
文件对象的声明比较特殊。假设我们要声明一个文件对象“f”,可以使用一个全局函数open来指定它的路径、模式、编码。
其中,第三个参数可以省略,例如:
在open函数调用完毕之后,f指向本地的某个文件,f相当于将一个对象引用到文件(参考1.4.1节)。可以通过type()来检查f的类型。
测试发现f的类型是“_io.TextIOWrapper”,并不是我们认为的“file”。这点请注意!
3.文件读操作
当使用 open 函数打开一个文件后,会返回一个文件对象,可以使用文件对象的方法完成对文件的读、写等操作。文件对象的常用方法如表3-3所示。
表3-3 文件对象的常用方法
说明:对文件操作的时候究竟是字符还是字节,取决于当前操作文件的类型或读取方式,若有“b”就是二进制形式,读取的是字节;若没有“b”就是文本形式,读取的是字符。本章主要讨论文本文件的读、写操作。
【例3-4】 读取文件的所有内容。
此时,输出的内容前多了“\ufeff”,但通过 print()输出时不会出现,所以不用理会。当然,如果的确希望得到如下的结果:
那就需要修改open函数中的编码参数,如下:
观察输出的文件内容,发现在换行的位置并没有显示为换行,而是通过一个转义字符“\n”来显示。换行显示为“\n”,这是控制台交互式方式下提示符的表现形式。如果希望在控制台屏幕上显示为换行结果,则可以使用 print()来输出 f.read()的结果。但在上次使用完 f.read()后,若再次使用即第二次调用 f.read()时请注意,此时得不到想要的结果,得到的是空白。原因在于,它的内部机制是文件读取的时候有一个指针从开始移到结尾,read()结束之后,文件指针已经移到文件尾了。当再次执行 read()时,已经没有内容可读取了。针对这种情况,有两种解决方法。
方法一:重新创建当前文件的实例(重新进行声明),然后进行读取,但这样比较麻烦。
方法二:把文件指针重新移到文件的开头,即调用 f.seek(0)(它表示将指针移到文件的开头,也就是第一个字符位置),然后重新调用f.read()来完成。
通过调用 f.seek(0)将文件指针重新移到文件最开始的位置,再次对文件进行操作,这对于规模较小的文件是可以的。但若文件规模较大,这样的读取方式就不可行了,因为它会占用内存,读取的效率不高,而且当希望对读取的文件内容进行进一步的处理时比较麻烦。因此,可以考虑使用别的方法。
下面来看如何把一个规模较大的文件先读入一个列表中,然后再针对每一行进行处理。
【例3-5】 将文件所有行读取到列表。
如果只是希望将文件每一行的内容输出,则可以通过一个更简单的方法来解决。因为 Python 将文件本身作为一个行序列,所以,通过 for-in 遍历循环可以直接输出文件每一行的内容。
对文件也可以不调用任何方法来完成这些操作,原因在于,声明一个文件对象后,得到的文件指针是一个可迭代的对象,可用for-in遍历循环对其进行遍历操作。
3.1.2 编码问题
在前面的例子中,利用 open 函数来声明文件对象时,省略了第三个参数,即省略了编码的指定。接下来介绍指定编码后会出现什么问题,以及该如何处理。
首先,打开文件“D:\python\PythonEXample\data.txt”,另存为“D:\python\PythonEXample\data1.txt”,但在保存时选择编码方式为“UTF-8”。
对“D:\python\PythonEXample\data1.txt”文件进行操作:
结果显示在解码的时候遇到一些问题,原因是之前我们保存的“data.txt”的编码方式是“gbk”,而“data1.txt”的编码方式是“UTF-8”,两者的编码方式不兼容。
这时可以通过在使用open函数时明确指明它的第三个参数来解决这个问题。
因此,今后凡是出现UnicodeDecodeError的错误(编码错误),首先想到的就是在打开文件时指定的编码方式和保存文件时指定的编码方式不兼容。此时只需要通过open函数中的第三个参数指定正确的编码方式就可以了。
注意:文件打开时如果省略第三个参数,则默认以“gbk”的编码方式打开,因此,如果保存文件时不是以“gbk”的方式保存的,请在打开文件时一定记得带上参数“encoding”来指定编码方式。如果文件是以“Unicode”方式保存的,则打开文件时指定的编码参数为“encoding="UTF-16"”。
根据以上对全局函数open的操作,对于open("路径","模式",encoding=''编码"),总结如下:
(1)第二个参数、第三个参数都可以省略。
(2)省略第二个参数,默认以'r'模式打开。
(3)省略第三个参数,默认以“gbk”的编码方式打开。
(4)打开文件时指定的编码方式一定要和保存文件时的编码方式一致。
3.1.3 文件写入操作
前面主要介绍了Python对文本文件的读取操作,下面介绍如何对文件进行写入操作。
现在使用 Python 来创建文本文件。假定要操作文件的位置还是在“D:\python\PythonEXample”目录下,为了避免在接下来的操作中写完整的路径,我们把当前的工作目录切换到“D:\python\PythonEXample”目录下,可以通过导入 os 模块来实现。
1.write()方法
假设我们希望在“D:\python\PythonEXample”目录下创建一个文件,保存一些信息,如写入一个特定的字符串信息,则可以通过调用文件对象的“write()”方法来完成(其参数必须是字符串!)。注意:此时若想换行,必须明确指定一个换行符“\n”来进行换行,代码如下:
执行上述代码后,到指定目录下可以看到的确有一个“course.txt”文件,如图 3-2所示。但双击打开该文件,发现文件里没有任何文字内容。
注意:open 函数的'w'模式只能创建文件,不能创建文件夹。如果要创建文件夹,请调用“os.mkdir(path)”或“os.makedirs(path)”来完成。
图3-2 查看建立的文件
2.close()方法
上面用 write()创建的文件之所以没有内容,原因在于刚才的代码还没有编写结束,其实刚才的操作还只是在内存里的操作。如何才能将刚才写入的内容“单位:重庆师范大学\n”真实地反映在具体的文件里呢?方法是关闭刚才操作的文件(关闭连接)。完整的代码如下:
此时再重新打开文件“course.txt”,就可以看到刚才写入的内容了。
打开文件后,可观察到光标所在位置是在写入内容的下一行,请问为什么?
打开文件后,通过“另存为”对话框将会看到,其编码方式的确为“UTF-8”。
3.writelines()方法、flush()方法
如果要一次写入多行文本,可以事先将多行文本放到一个列表里,然后调用writelines()方法,它可以一次性地将列表中所有的信息写入文本文件中。
同样,代码还在内存里,它并没有直接映射输出到文件中。此时如果我们不想关闭文件,而又想将缓存的内容映射到硬盘上,则可使用方法flush()来达成目标。
注意:没有“writeline()”方法!
当打开所创建的文件后又发现新的问题,本来希望把每一个姓名写入文件的每一行中,但是发现所有的内容都写在一行上了,显示结果如下:
此时只需要修改“names”变量,在其每个元素中加上一个换行符“\n”即可。
3.1.4 列表推导式
上面给出的加换行符的方法不推荐使用,因为如果 names 的元素个数很多,这样的修改操作显然不可取。下面介绍一种非常简便的方法——列表推导式。
1.列表推导式书写形式
列表生成式(List Comprehensions),又叫列表推导式,是Python内置的非常简单却强大的可以用来创建列表的生成式[3],它是利用其他可迭代序列来创建新列表的一种方法。它的工作方式类似于for-in遍历循环。其语法格式如下:
此处的“表达式”可以是有返回值的函数。
说明:表达式中的变量来自 for-in 遍历循环中的变量,随着变量在迭代序列中的遍历,将遍历得到的值带入表达式,表达式的值将作为列表中元素的值。
列表推导式的本质是从可迭代序列中选出一部分或全部元素进行运算后作为新列表的元素,从而生成一个新的列表。注意,生成的是另外一个新列表,原列表保持不变。利用列表推导式能非常简洁地构造一个新列表。
2.示例
【例3-6】 利用range函数生成一个由0~9每个数的平方作为元素的列表。
说明:这里表达式“x*x”里的 x 来自 for-in 遍历循环中的变量,而该变量 x 在“range(10)”产生的序列 0,1,2,…,9 中依次取值,并将每次遍历的值代入表达式“x*x”进行计算,然后将“x*x”的值作为最后得到的列表中的元素。所以,最后得到的列表为[0,1,4,9,16,25,36,49,64,81]。
如果希望得到的列表元素是能被 3 整除的数的平方,则在列表推导式中添加一个if表达式就可以完成。
【例 3-7】 利用 range 函数生成由 10 以内且能被 3 整除的数的平方作为元素的列表。
第一次循环时,变量x取0,此时条件0%3==0成立,因此,将此x的值带入表达式“x*x”计算得到值 0,作为列表的第 1 个元素;第二次循环时,变量 x 取 1,但此时条件 1%3==0 不成立,因此,不带入表达式“x*x”进行计算;第三次循环时,变量 x 取 2,此时,条件 2%3==0 仍然不成立,因此,也不带入表达式“x*x”进行计算;第四次循环时,变量 x 取 3,条件 3%3==0 成立,因此,将其带入表达式“x*x”计算得到值9,作为列表的第2个元素;……最后得到的列表为[0,9,36,81]。还可以增加更多的for语句来实现更为复杂的功能。
【例3-8】 利用range函数生成由数字0和1两两组合形成的列表作为元素的列表。
【例3-9】 利用range函数生成由数字0~2两两组合形成的元组作为元素的列表。
列表推导式总是返回一个列表。
【例 3-10】 遍历元组(或列表)的每个元素,得到由元组(或列表)的每个元素的平方构成的列表。
3.用列表推导式解决问题
问题:如3.1.3节最后提到的,我们希望把每一个姓名写入文件的每一行中,而不是把所有的内容都写在同一行。
要解决以上问题,可以重新声明一个变量,但这里我们使用列表推导式来完成。新的列表 new_names 的元素等于之前的列表 names 中的元素 name 加上一个换行符“\n”,新的列表中的变量name来自之前的列表names中的元素。
这里,表达式为“name+'\n'”,表达式里的变量name来自for-in遍历循环里的变量 name,而变量 name 在循环过程中会遍历列表 names 中的所有元素,每遍历出单个元素都把它当作临时变量name代入表达式“name+'\n'”中计算,得到的结果作为新列表里的一个元素,即在列表 names 中的每个元素后加上'\n',最终返回一个新的列表new_names。
此时再打开文件“people.txt”可看到结果如下:
思考:假设之前已经在一行写入了 4 个人名信息,现在再次写入之后,为什么不是位于原有内容之后,而是把原来的内容替换掉了呢?请给出能得到正确结果的代码。
提示:希望大家打开资源管理器窗口,仔细观察文件操作过程中每个命令执行时它的变化情况。
3.1.5 关闭文件
虽然通过调用方法 flush()能够将缓存的内容写到文件中,但这里再次强调,最终文件的关闭还是要调用close()方法来完成。
在 Python 中,close()方法自动进行垃圾回收,释放资源,所以,为了养成一个好的编程习惯,或者说考虑到 Python 语言的不同实现,应该养成手动关闭文件的习惯。但是写代码时往往容易忽略这个操作,此时可利用Python提供的上下文语法来实现[2]。
3.1.6 上下文语法
Python 中的上下文语法,具体来说是通过一个特定的代码段,将一系列的操作封装在一个上下文的环境里(用关键字 with 进行封装),当这个环境结束时,它会自动调用close()来关闭,而不需要我们手动去调用close()了。
上下文语法的格式如下:
缩进代码体的操作都是围绕对象f进行的。
这样就不用手动调用f.close()来显式地关闭文件了。在当前的上下文代码体执行完毕后,当前的资源会自动释放。
【例 3-11】 假定对某个文件要进行读取操作,而又不想显式地调用 close()关闭文件,请利用上下文语法来实现。
如想读取“people.txt”文件的内容,通过 open 函数打开一个文件,把它放到一个上下文对象f里,f不调用任何方法的时候其实是调用它本身的迭代对象,可以遍历打印该迭代对象的所有内容。打印完退出整个“with”上下文代码体的时候,不用调用f.close(),系统会自动关闭,并且释放所需要的资源。代码如下:
以上看到的是读取操作,同样,写入操作也可类似地完成。
以上代码并没有包含 f.close()或 f.flush(),但是执行上述代码后,打开文件发现要写入的信息已经写入文件“test.txt”中了,说明执行上下文代码体后系统自动调用了close()方法关闭文件。
实际开发时上下文语法比较实用,它可以避免显式地调用 close()或编写释放资源的代码。
3.1.7 生成器
通过列表推导式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。创建一个规模很大的列表,不仅占用的存储空间多,而且如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白地浪费了。
所以,如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?如果可以这样,就不必创建完整的列表,从而可以节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器(generator)。
1.创建生成器
要创建一个生成器,有很多种方法。这里介绍一种很简单的方法,只要把一个列表推导式的中括号[]改成小括号(),就创建了一个生成器。
说明:只需要把创建列表推导式的中括号[]改成小括号()即得到了生成器,但注意,不是把创建列表的[]改成(),那样得到的是一个元组。所以,要清楚列表的创建和列表推导式的创建是不同的,虽然最后的结果都是一个列表。
2.获取生成器的每个元素
由于列表推导式最终的结果还是一个列表,因此,可以通过下标索引的方式对列表中的元素进行访问和截取。但对生成器中的元素,又该如何来访问呢?
由于生成器保存的是算法,因此,可以通过调用全局函数 next()来获得生成器的每个元素[4],直至计算到最后一个元素。若此时再次使用 next()来获取元素,系统会抛出 StopIteration 异常。实际上,可以把生成器的数据流看作一个有序序列,虽然不知道序列的长度,但是可以通过不断地计算来获取下一个值,直到最后抛出StopIteration异常。StopIteration异常用于标识迭代的完成,防止出现无限循环的情况。
但要特别注意:一旦生成器的值用完了,再次调用 next()就会出现异常错误,所以,每个生成器只能使用一次。
请仔细理解下面的每步操作。
更多next()函数的使用方法可以通过help(next)来了解。
通过next()函数虽然可以输出生成器的每个元素,但获取完最后一个元素后如果还试图执行 next()操作,系统会抛出 StopIteration 异常。所以,最好的方式是通过 for-in遍历循环来输出生成器的每个元素,使用这种方式系统不会抛出异常。
也可以用下面的方式来达到同样的目标:
生成器非常强大。当推算的算法比较复杂,用类似列表推导式的 for-in 遍历循环无法实现时,还可以用函数来实现。更多有关生成器的内容请大家查阅相关网站[3]或查看 Python 的官方文件[4]。生成器不仅可以使用 for-in 遍历循环输出每个元素,还可以通过不断调用 next()函数返回下一个值,直到最后抛出 StopIteration 错误,表示已经读取完生成器中的所有元素了。