一切皆文件
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() # 关闭
注意:
- 文件内容的编码需要和Python默认字符编码兼容才能正常打印,否则会抛出字符编码非法异常
- 如果
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=-1 | t和b,都是io.DEFAULT_BUFFER_SIZE |
buffering=0 | b关闭缓冲区;t不支持 |
buffering=1 | b是1个字节;t是行缓冲,遇到换行符才flush |
buffering>1 | b模式表示行缓冲大小,缓冲区的值可超过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可保证打开的文件被关闭。
-
上下文管理
一种特殊的语法,交给解释器去释放文件对象。
- 使用with…as关键字
- 上下文管理的语句并不会开启新的作用域
- 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被打开时,会获得一个文件描述符。计算机资源有限,所有操作系统都会对其做限制,这是为了保护计算机资源不被完全耗尽。一般情况下,除非特别明确知道资源情况,否则不要提高资源的限制值来解决问题。