Python文件IO基础,看这篇文章就够了!

一切皆文件

Linux的设计哲学之一:一切皆文件。

因此,设备也是文件,对设备的操作可以转换成对文件的I/O操作。

冯诺依曼体系架构

在这里插入图片描述

  • CPU由运算器和控制器组成,计算机的五大部件如下:
    • 运算器:完成各种算数运算、逻辑运算、数据传输等数据加工处理
    • 控制器:控制程序的执行
    • 存储器:用于记忆程序和数据,如内存
    • 输入设备:将数据或程序输入到计算机中,如键盘、鼠标
    • 输出设备:将数据或程序的处理结果展示给用户,如显示器、打印机

一般地,IO操作直的是文件IO。如果指的是网络IO,会直接说成网络IO。

文件操作

文件的概念

在磁盘中存储为一个个字节,形成字节流bytes。其存储位置很可能是随机存放,需要通过磁头的转动来访问数据,此时为随机访问。

文件常用操作

名称含义
open打开文件
read读取全部内容
write写入内容
close关闭文件
readline单行读取
readlines多行读取
seek文件指针操作
tell指针位置

open

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

打开一个文件,返回一个文件对象(流对象)和文件描述符。若打开文件失败,则返回异常。

基本使用:创建一个文件,然后打开它,用完关闭。

In [1]: f = open('test')  #建立文件对象,文件必须事先存在                                                 
In [2]: type(f)                                                            
Out[2]: _io.TextIOWrapper

In [3]: f.read()        # 读操作,读取全部内容                                                 
Out[3]: 'This is test file for PyIO\n'

In [4]: f.close()      # 关闭

注意:

  1. 文件内容的编码需要和Python默认字符编码兼容才能正常打印,否则会抛出字符编码非法异常
  2. 如果f.open()文件后,其他编辑器对该文件修改,此时f.read()只能看到修改前的内容。重新打开才能看到新 内容。

文件操作中,最常用的操作就是读和写。

文件访问的模式有两种:文本模式和二进制模式。不同模式下,操作函数不尽相同,表现的结果也不一样。

open的参数
file

打开或者要创建的文件名。如果不指定路径,默认是当前路径。

mode

打开模式:

描述字符含义
r缺省,表示只读打开
w只写打开
x创建,并写入一个新文件
a写入打开。如果文件存在,则追加
b二进制模式
t缺省,文本模式
+读写打开一个文件。给原来只读、只写方式打开提供缺失的读或写能力

上例中,可知默认是文本模式打开,且是只读。

  • r

    表示只读打开,如果使用write方法,会抛异常

    如果文件不存在,则抛出FileNotFoundError异常

  • w

    表示只写打开,如果使用read方法,会抛异常

    如果文件不存在,则直接创建文件

    如果文件存在,则清空文件内容

  • x

    文件不存在,则创建文件,并以只写方式打开

    文件存在,则抛出FileExistsError异常

  • a

    文件存在,则只写打开,追加内容

    文件不存在,则创建后,只写打开,追加内容

    提示:

    r是只读,wxa都是只写。

    wxa都可产生新文件,w不管文件存在与否,都会生成全新内容的文件;a不管文件是否存在,都能在打开的文件尾部追加;x必须要求文件事先不存在,自己造一个新文件

  • 文本模式t

    字符流,将文件的字节按照某种字符编码理解,按照字符操作。open的默认mode为rt

  • 二进制模式b

    字节流,将文件就按照字节理解,与字符编码无关。二进制模式操作时,字节操作使用bytes类型。

    提示:字符转字节,需要encode()编码;反之,则decode()解码

二进制模式的读取和写入

In [1]: f = open('test','rb')    # 二进制模式读取                                                    
In [2]: f.read()                                                                     
Out[2]: b'This is test file for PyIO\n' # 读取的类型字节类型bytes

In [3]: f1 = open('test2','a')  # test2文本模式追加                                 

In [4]: f1.write('啊')    # 文本                                                            
Out[4]: 1

In [5]: f.close()                                                                    
In [6]: f1.close()                                                           

In [7]: !cat test2                                                                   
啊
In [8]: f = open('test2','rb')   # test2二进制模式读取                                            
In [9]: f.read()                                                                     
Out[9]: b'\xe5\x95\x8a' # 字节类型,对应ASCII
In [1]: f = open('test3','wb')             # 二进制模式写入                                       
In [2]: f.write(b'啊')         # 必须以ASCII码的方式写入                                                      
  File "<ipython-input-2-5f74f1f1adca>", line 1
    f.write(b'啊')
           ^
SyntaxError: bytes can only contain ASCII literal characters.

In [3]: f.write('啊'.encode())     # 文本模式encode为二进制字节流类型                                                  
Out[3]: 3

In [4]: f.close()                                                                    
In [5]: !cat test3                                                                   
啊

功能增强+

为r、w、a、x提供缺失的读写能力。但是,获取文件对象依旧按照r、w、a、x特征,+不能单独使用,可认为它是为前面的模式字符增强功能的。

读写访问r+

In [15]: f = open('test','r+')     # 添加缺失的写能力,要求文件事先存在                                           

In [16]: f.write('abcdefg')      # 在开头写入                                                    
Out[16]: 7

In [17]: f.read()                                                                    
Out[17]: ' test file for PyIO\n' # 文件指针指向写入内容后,因此只能看到后面的内容

In [18]: f.close()                                                                   

In [19]: !cat test                                                                   
abcdefg test file for PyIO

可指定读取字符个数或字节个数:

In [35]: f = open('test','r+')                                                       

In [36]: f.read(1)                                                                   
Out[36]: '啊'

In [38]: f = open('test2','rb')                                                      

In [39]: f.read(1)                                                                   
Out[39]: b'\xe5'

读写访问w+

In [20]: f = open('test','w+')                                                       

In [21]: f.read()                                                                    
Out[21]: ''

In [22]: f.write('啊 Python')                                                        
Out[22]: 8

In [23]: f.read()                                                                    
Out[23]: ''

In [24]: f.close()                                                                   

In [25]: !cat test                                                                   
啊 Python

二进制的r+,w+

In [81]: f = open('test3','wb+')                                                                                         
In [82]: f.write('啊Python'.encode())                                                                                    
Out[82]: 9

In [83]: f.close()                                                                                                       

In [84]: !cat test3                                                                                                      
啊Python

In [85]: f = open('test3','rb+')                                                                                         

In [86]: f.read()                                                                                                        
Out[86]: b'\xe5\x95\x8aPython'

In [87]: f.write('啊python'.encode()) 追加内容                                                                                    
Out[87]: 9

In [88]: f.close()                                                                                                       
In [89]: !cat test3                                                                                                      
啊Python啊python

注意:

如果读写能力都具备,使用r打开。如果打开后直接写,则从头开始写。如果打开后先读取,然后再写,则从尾部写。实际上是文件指针的原因。

追加 a+

直接从尾部开始追加内容,或者直接从尾部读取内容,因此是无法读取到任何内容的。

也可使用x+

文件指针

文件指针,指向当前字节位置

mode=r,指针起始在0

mode=a,指针起始在EOF

tell()显示指针当前位置

seek(offset[,whence])

移动文件指针位置。offset偏移多少字节,whence从哪里开始

whence 0缺省值,表示从头开始,offset只能正整数

whence 1 表示从当前位置,offset只接受0,比如f.seek(0,1) 表示指针在文件开头或当前位置

whence 2 表示从EOF开始,offset只接受0,比如f.seek(0,2)表示指针在文件结尾

In [2]: f = open('test','r')                                                         

In [3]: f.tell()                                                                     
Out[3]: 0

In [4]: f.read(1)                                                                    
Out[4]: '啊'

In [5]: f.tell()                                                                     
Out[5]: 3

In [6]: f.read(1)                                                                    
Out[6]: ' '

In [7]: f.tell()                                                                     
Out[7]: 4

指针一边写,一边移动。因此若f.write(‘xyz’)后,f.read()无法读取到内容,因为指针已经移动到结尾。此时可以seek重新指定指针的位置。

In [8]: f.seek(0)                                                                    
Out[8]: 0
In [10]: f.read()                                                                    
Out[10]: '啊 Python'

注意下面的特性:如果在数据中间覆盖,会自动追加到末尾。

In [12]: f = open('test','r+')                                                       
In [13]: f.read()                                                                    
Out[13]: '啊 Python'
In [14]: f.seek(3)                                                                   
Out[14]: 3
In [15]: f.read(3)    # 指针移动到Py处                                                     
Out[15]: ' Py'
In [16]: f.write('THON')     # 在Py处写入                                                  
Out[16]: 4
In [17]: f.seek(0)                                                                   
Out[17]: 0
In [18]: f.read()                                                                    
Out[18]: '啊 PythonTHON'  # 追加到末尾

文本模式

文本模式支持从开头向后偏移的方式。

whence为1表示从当前位置开始偏移,但只支持偏移0,相当于原地不动。

whence为2表示从EOF开始,只支持偏移0,相当于移动文件指针到EOF。

seek是按照字节偏移的。

二进制模式

whence 0 缺省值,表示从头开始,offset只能是正整数

whence 1表示从当前位置,offset可正可负

whence 2表示从EOF开始,offset可正可负

二进制模式支持任意起点的偏移,从头、从尾、从中间位置开始均可。

向后seek可超界,但是向前seek时不可超界,否则抛出异常。

缓冲区

-1表示使用缺省大小的buffer。如果是二进制模式,使用io.DEFAULT_BUFFER_SIZE值,默认是4096或者8192。如果是文本模式,如果是终端设备,则是行缓存方式,否则,使用二进制模式策略。

  • 0只在二进制模式使用,关闭关闭buffer
  • 1只在文本模式使用,表示使用行缓冲。意思为遇到换行符就flush。
  • 大于1用于指定buffer的大小。

buffer缓冲区

缓冲区是一个内存空间,一般而言是一个FIFO队列,到缓冲区满了或者达到阈值时,数据才会flush到磁盘中。

flush()将缓冲区数据写入磁盘

close()关闭前会调用flush()

io.DEFAULT_BUFFER_SIZE缺省缓冲区大小,单位是字节

In [1]: import io                                                                    
In [2]: print(io.DEFAULT_BUFFER_SIZE)                                                
8192

文本模式:默认开启buffer,默认为-1.

如果是1,则表示使用行缓冲

读写打开:w+
In [3]: f = open('test','w+')                                                        

In [4]: !cat test                                                                    

In [5]: f.write('!'*10)                                                              
Out[5]: 10

In [6]: f.read()                                                                     
Out[6]: ''

In [7]: !cat test                                                                    
!!!!!!!!!!

即使没有flush,因为write执行完毕了,相当于程序执行完毕了,系统会自动触发缓冲清理,就会触发flush。

如果while True,只要程序没执行完毕,则必须写满缓冲区才会flush。

只写打开:w
In [3]: f = open('test','w')                                                         

In [4]: f.write('!' * 1024)                                                          
Out[4]: 1024

In [5]: !cat test 

只写方式打开,不会马上刷写到磁盘,无论遇到换行符,直到缓冲区溢出为止。

二进制模式:默认为-1,启动buffer

如果是二进制模式,默认buffer模式为-1,启动buffer

In [14]: f = open('test','wb+')                                                      

In [15]: !cat test                                                                   

In [16]: f.write(b'!'*1024)                                                          
Out[16]: 1024

In [17]: !cat test   

内容写到了buffer中。

如果关闭buffer,置为0,一旦写入则马上落到磁盘中。

In [19]: f = open('test','wb+',0)                                                    
In [20]: !cat test                                                                   
In [21]: f.write(b'abc')                                                             
Out[21]: 3

In [22]: !cat test                                                                   
abc

一般,都需要打开缓冲区。

指定buffer大小:仅限于二进制模式下

在文本模式下,指定的buffer size无效。

指定buffer大小,比如指定为4字节:

In [24]: f = open('test','wb+',4)                                                    

In [25]: !cat test                                                                   

In [26]: f.write(b'abc')                                                             
Out[26]: 3

In [27]: !cat test                                                                   

In [28]: f.write(b'de')                                                              
Out[28]: 2

In [29]: !cat test                                                                   
abc

缓冲区超过的部分,会把此部分置回到磁盘中。

注意:文本模式无法关闭缓冲区。

Buffering 总结

buffering=0

这是一种特殊的二进制模式,不需要内存的buffer,可看做是一个FIFO的文件。

In [85]: f = open('test4','wb+',0)                                                          

In [86]: f.write(b"m")                                                                      
Out[86]: 1

In [87]: !cat test4                                                                         
m
In [88]: f.write(b"a")                                                                      
Out[88]: 1

In [89]: !cat test4                                                                         
ma
In [90]: f.write(b"g")                                                                      
Out[90]: 1

In [91]: !cat test4                                                                         
mag

如果字节流关闭buffer,相当于先进先出的文件。此时一直在尾部追加数据,一般不使用f.seek()重新移动指针,否则FIFO失去意义。

如果buffer的模式为1,则用于文本模式下的行缓冲。缓冲区一旦满,则会刷写到磁盘中。不过,只要给定换行符,则会马上把内容刷写到磁盘中。

buffering说明
buffering=-1t和b,都是io.DEFAULT_BUFFER_SIZE
buffering=0b关闭缓冲区;t不支持
buffering=1b是1个字节;t是行缓冲,遇到换行符才flush
buffering>1b模式表示行缓冲大小,缓冲区的值可超过io.DEFAULT_BUFFER_SIZE的值,直到溢出后把超过部分flush到磁盘中;t模式,是io.DEFAULT_BUFFER_SIZE。

总结:

  • 文本模式下,一般都用默认缓冲区大小
  • 二进制模式下,是一个个字节操作,因此可指定buffer的大小
  • 一般来说,默认缓冲区大小是个比较好的选择,除非明确知道自己在做什么,否则不要调整
  • 一般编程中,明确知道需要写磁盘,都会手动调用一次flush

encoding:编码,仅在文本模式使用

None表示使用缺省编码,依赖操作系统。windows下缺省编码为GBK(0xB0A1),Linux下缺省UTF-8(0xE5 95 8A)

在文本模式下,要求统一编码,比如全部使用UTF-8:英文为一个字节,为ASCII;而中文则是三个字节,某些字可能使用四个字节。

在跨平台的场景中,使用UTF-8比较合适和通用。但是在存储中文时,由于它多用了字节,因此会增大存储压力。而且在多副本的情况下,存储效率会降低,并且增加网络传输的字节数,这是它的坏处。

Unit code

Unit Code:用字节,表示世界上所有的文字。而且是全球统一编码,它使用两个字节,全部表示了全世界的文字,而且它把ASCII也使用了两个字节来表示。但是这样比较浪费存储空间。因此,它不太能推行得下去。

到了网络时代,出现了UTF(通过Unit Code转换而来)。而UTF-8,则是8位传递的。

编码的相互转换:unitCode -> GBK。此时需要编码转换表。

open的其他参数

errors

什么样的编码错误将被捕获

None和strict表示有编码错误将抛出ValueError异常;ignore表示忽略。

newline

文本模式下,换行的转换。可以为None、‘空串’、’\r’、’\n’、’\r\n’

读取时,None表示’\r’、’\n’、’\r\n’都被转换为’\n’;空串’'表示不会自动转换通用换行符;其他合法字符表示换行符就是指定字符,按照指定字符分行。

写入时,None表示’\n’都会被替换为系统缺省行分隔符os.linesep;’\n’或空串’‘表示’\n’不替换;其他合法字符表示’\n’会被替换为指定字符

In [7]: import os                                                                    

In [8]: os.linesep                                                                   
Out[8]: '\n'

In [9]: f = open('test','w')                                                         

In [10]: f.write('hello\npython!\r\n')                                               
Out[10]: 15

In [11]: f.close()                                                                   

In [12]: !cat test                                                                   
hello
python!

In [13]: f = open('test','r')                                                        

In [14]: f.read()                                                                    
Out[14]: 'hello\npython!\n'

In [15]: f.close()                                                                   

In [16]: f = open('test','rb')                                                       

In [17]: f.read()                                                                    
Out[17]: b'hello\npython!\r\n'

closefd

关闭文件描述符,True表示关闭它。Flase会在文件关闭后,保持这个文件描述符。可用fileobj.fileno()查看。

文件描述符:文件打开后会占用一个文件对象,它和文件建立连接。文件对象保存了文件描述符。比如0,1,2分别代表标准输入,标准输出,错误输出的文件描述符。

如果文件关闭后,文件描述符会被回收。

读、写、关闭和其他方法

read 字符/字节读取

read(size=-1)

size表示读取的多少个字符或字节;负数或者None表示读取到EOF。

文本模式下为字符读取;二进制模式下为字节读取。

# 文本模式
In [12]: f = open('test','r+')                                                                                 
In [13]: f.read(3)                                                                                             
Out[13]: 'www'

# 二进制模式:
In [15]: f = open('test','rb+')                                                                                
In [16]: f.read(3)                                                                                             
Out[16]: b'www'

行读取的问题:f对象,read,readline(),readlines()

readline(size=-1)

一行行读取内容,size设置一次能读取行内几个字符或者字节。

readlines(hint=-1)

读取所有行的列表,指定hint则返回指定的函数。

In [18]: f = open('test','r+')                                                                                 
In [19]: f.readline()    # 按行读取                                                                             Out[19]: 'www.python.org\n'

In [20]: f.readline()                                                                                           
Out[20]: 'www.jaywin.com\n'

In [21]: f.seek(0)                                                                                             
Out[21]: 0

In [22]: f.readlines()      # 读取所有行,返回列表                                            Out[22]: ['www.python.org\n', 'www.jaywin.com\n', '\n']

In [23]: f.seek(0)                                                                                            
Out[23]: 0

In [24]: f.readline(1)    # 读取指定字符数                                                   Out[24]: 'w'

In [25]: f.readline(1)                                                                                   
Out[25]: 'w'
In [27]: for line in f.readlines(): 
    ...:     print(line.strip()) 
    ...:                                                                                                          
www.python.org
www.jaywin.com

有些初学者往往将以下的情况混淆
假设一个文件的内容如下:

This is first line
This is second line
and so on...

仔细领悟下面四种读取文件内容方式的异同:

f = open('test.txt','r+')
for line in f:
    print(line)
->
This is first line

This is second line

and so on...
f = open('test.txt','r+')
for line in f.read():
    print(line)
->
T
h
i
s 全部内容
...
f = open('test.txt','r+')
for line in f.readline():
    print(line)
->
T
h
i
s  一行
...
f = open('test.txt','r+')
for line in f.readlines():
    print(line)
->
This is first line

This is second line

and so on...

可以看出,f对象和f.readlines()单行打印的内容是一致的。实际上,f对象是一个迭代器,支持next(),而f.readlines()则返回列表。

write

write(s),把字符串s写入到文件中,并返回字符的个数

writelines(lines),把字符串列表写入文件中。注意是字符串列表。

In [29]: f = open('test','w+')                                                                                  In [30]: lines = ['abc','123\n','jaywin.com']                                                                    In [31]: f.writelines(lines)                                                                                    In [32]: f.seek(0)                                                                                              Out[32]: 0

In [33]: f.read()                                                                                                Out[33]: 'abc123\njaywin.com'

close

flush并关闭文件对象,此时会把文件描述符归还给操作系统。

若文件已关闭,再次关闭则无任何效果。

其他

seekable() 是否可seek

readable() 是否可读

writeable() 是否可写

closed() 是否已关闭

上下文管理

问题的引出:

f = open('test','w+')
f.write(str(1/0))
print('~~~')
f.close()

考虑上述例子,抛出异常后程序退出,并没有释放文件描述符。如果存在大量的异常退出,将会占据操作系统的文件描述符资源。

在Linux中,可通过ulimt -n命令查看文件最大打开数。

如果文件描述符不及时释放,会导致文件描述符占用过多。如果程序在正常关闭前抛出异常,则程序自动终止,来不及释放文件描述符。

处理方式:

  • 异常处理

    当出现异常时,拦截异常。但是,很多代码都可能出现OSError异常,不好判断异常是否因为资源限制产生。

    f = open('test')
    try:
    	f.write("abc")
    finally:
    	f.close()
    

    使用finally可保证打开的文件被关闭。

  • 上下文管理

    一种特殊的语法,交给解释器去释放文件对象。

    1. 使用with…as关键字
    2. 上下文管理的语句并不会开启新的作用域
    3. with语句执行完毕后,会自动关闭文件对象
    f = open('test','w+')
    with f:
    	f.write('efg')
    	f.write(str(1/0)) # error
    

    即使执行失败,也会自动关闭文件描述符。

    其中,with是一个语法糖。

    所有文件对象都支持上下文管理,支持with。

    另一种写法:

    with open('test') as f: # 把文件对象重命名为f
    	f.write('efg')  # f对象写
    	f.write(str(1/0)) # 保证文件对象退出时,关闭文件对象
    

​ 对于类似于文件对象的IO对象,一般都需要在不使用时关闭、注销以释放资源。IO被打开时,会获得一个文件描述符。计算机资源有限,所有操作系统都会对其做限制,这是为了保护计算机资源不被完全耗尽。一般情况下,除非特别明确知道资源情况,否则不要提高资源的限制值来解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值