9. IO 编程
涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
程序完成IO操作会有Input和Output两个数据流。也有一个的时候,比如:从磁盘读取文件到内存,就只有Input;把数据写到磁盘文件里,就只有Output操作。
IO编程中,Stream(流) 是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream是数据从外面(磁盘、网络等)流进内存,Output Stream是数据从内存流到外面去。
对于浏览网页来说,浏览器和xx服务器之间至少需要建立两根水管,才可以既能发送数据又能接收数据。
同步IO:程序按执行顺序运行,遇到耗时操作CPU会等待
异步IO:CPU利用率大,遇到IO执行耗时操作时不会等待而是执行其他操作
区别:是否等待IO执行的结果。使用异步IO编写程序性能会远远高于同步IO,但异步IO的缺点是编程模型复杂。
demo:点汉堡,服务员说需要5分钟,我等5分钟拿到汉堡后去逛商场,属于同步IO;服务员说我先去逛商场,汉堡做好通知我,这样我可以立刻去逛商城,属于异步IO。服务员跑过来找我说汉堡做好了,属于 回调模式,服务员发短信通知我,我得不停的检查手机,属于 轮询模式。异步IO的复杂度远远高于同步IO。
操作IO的能力都是由 操作系统 提供的,每种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。
1. 文件读写
读写文件是最常见的IO操作。
读写文件是请求操作系统打开一个文件对象(通常称为文件描述符),然后通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读文件
方式一:Python内置的open()
函数
Python内置的open()
函数以读文件的模式打开一个文件对象:
>>> f = open('Users/Jason/test.txt', 'r')
标识符’r’表示读。这样就打开了一个文件,如果不存在则抛出IOError
错误:
>>> f.read()
'Hello, world'
最后调用close()
方法关闭文件,文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的:
>>> f.close()
由于文件读写时都有可能产生IOError
,一旦出错,后面的f.close()
就不会调用,所以为了保证无论如何都正确关闭文件,需要加入try...finally
实现:
try:
f = open('Users/Jason/test.txt', 'r')
print(f.read())
finally:
if f:
f.close()
但这样很繁琐。
方式二:with
语句
with open('Users/Jason/test.txt', 'r'):
print(f.read())
这和前面的try...finally
是一样的,但是代码更简洁,并且不必调用f.close()
方法。
为防止读取的文件过大造成内存爆满,读取方法:
read()
:一次性读取文件的全部内容read(size)
:每次最多读取size
个字节的内容readline()
:每次读取一行内容readlines()
:一次读取所有内容并按行返回list
选择:
-
如果文件很小,
read()
一次性读取最方便; -
如果不能确定文件大小,反复调用
read(size)
比较保险 -
如果是配置文件,调用
readlines()
最方便for line in f.readlines():
print(line.strip()) # 把末尾的’\n’删掉
fine-like Object
fine-like Object:像open()
函数返回的这种有个read()
方法的对象。
StringIO
是在内存中创建的fine-like Object,常用作临时缓冲。
二进制文件
上边读取模式为'r'
的都是读取文本文件,UTF-8编码格式。要读取二进制文件,比如图片、视频等,用'rb'
模式打开文件:
>>> f = open('User/Jason/test.jpg', 'rb')
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节
字符编码
读取非utf-8编码的文本文件,需要给open()
函数传入encoding
参数,比如读取GBK编码的文件:
>>> f = open('User/Jason/test.jpg', 'r', encoding='gbk')
>>> f.read()
'测试'
还有一些编码不规范的文件可能会遇到UnicodeDecodeError
,open()
函数接收一个errors
参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
>>> f = open('User/Jason/test.jpg', 'r', encoding='gbk', errors='ignore')
写文件
读写文件一致,唯一区别是调用open()
函数时,传入标识符w
或wb
标识写文本文件或二进制文件:
>>> f = open('/User/Jason/test.txt', 'w')
>>> f.write('Hello world')
>>> f.close()
可反复调用write()
写入文件,但务必要调用f.close()
关闭。
当写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存中缓存起来,空闲的时候再慢慢写入。只有调用close()
方法时,操作系统才能保证把没有写入的数据全部写入磁盘。忘记调用f.close()
的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以还是用with
语句来得保险:
with open('User/Jason/test.txt', 'w') as f:
f.write('Hello world')
要写入特定编码的文本文件,给open()
函数传入encoding
参数,将字符串自动转换成指定编码。
写入模式:
w
模式写入文件,如果文件存在则覆盖(相当于删除后新写入一个文件)wa
模式则是以追加(append)模式写入
读写文件的demo
# 读文件
# 1. 正常读取(正常需要加入try机制)
f = open('F:\\python\\python-basic\\test.txt', 'r')
print(f.read()) # read() 读所有
# print(f.read(3)) # read(n) 读第一行的 n 个字节
# print(f.readline()) # readline() 读一行
# print(f.readline(2)) # readline(n) 读一行的 n 个字节
# for循环打印所有内容
for line in f.readlines():
print(line.strip()) # strip() 把末尾的'\n'去掉
f.close()
# 2. with语句读取
with open('F:\\python\\python-basic\\test.txt', 'r') as ff:
# print(ff.read()) # 打印所有
print(ff.readline()) # 打印一行
# 写文件
# 1. 正常写入
fff = open('F:\\python\\python-basic\\test.txt', 'w') # 'w'覆盖写入
# fff = open('F:\\python\\python-basic\\test.txt', 'a') # 'a'追加写入
fff.write('python')
fff.close()
# 2. with语句写入
# with open('F:\\python\\python-basic\\test.txt', 'w') as ffff: # 覆盖写入
with open('F:\\python\\python-basic\\test.txt', 'a') as ffff: # 追加写入
ffff.write('haha')
小结:
在Python中,文件读写是通过open()
函数打开的文件对象完成的。使用with()
语句操作文件IO是个好习惯。
2. StringIO 和 BytesIO
1. StringIO
很多时候,数据读写不一定是文件,也可以在内存中读写。
StringIO是在内存中写str
,只能读写str
格式数据。
把str写入StringIO,需先创建一个StringIO
,然后像文件一样写入:
from io import StringIO
f = StringIO()
f.write('hello world!')
print(f.getvalue()) # getvalue()方法获 取写入后的str
# hello world!
# 如果运行在交互模式,write()会返回写入的字节数,比如f.write('hello')返回5
getvalue()
方法获取写入后的str
。
读取StringIO,用一个str初始化StringIO,然后像读文件一样读取:
from io import StringIO
f = StringIO('hello\nworld!')
# 正常读取
while True:
s = f.readline()
if s == '':
break
print(s.strip())
'''
hello
world!
'''
# for循环读取
for line in f.read():
print(line, end='')
'''
hello
world!
'''
2. BytesIO
BytesIO是在内存中读写二进制数据。
在内存中读写Bytes,创建一个BytesIO,然后写入一些Bytes:
from io import BytesIO
f = BytesIO()
f.write('中文'.encode('utf-8')) # 交互模式会返回6,因为utf-8格式,1个中文等于3个字节
print(f.getvalue())
# b'\xe4\xb8\xad\xe6\x96\x87'
注意:写入的不是str
,而是经过utf-8编码的Bytes
。
读取二进制数据跟 StringIO 类似,用一个 Bytes 初始化BytesIO,然后读文件:
from io import BytesIO
f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
print(f.read())
# b'\xe4\xb8\xad\xe6\x96\x87'
小结:
-
stringIO和BytesIO是在内存中操作
str
和bytes
的方法,使得和读写文件具有一致的接口 -
StringIO (str):
from io import StringIO
- 写:
f = StringIO() f.write('hello') f.getvalue() # 获取写入后的str,输出 hello
- 读:
f = StringIO('hello') # for循环输出,去掉空格。也可以直接输出f.read() for line in f.read(): print(line, end='')
-
BytesIO (Bytes):
from io import BytesIO
- 写:
f = BytesIO() f.write('中文'.endcode('utf-8')) f.getvalue() # b'\xe4\xb8\xad\xe6\x96\x87'
- 读:
f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') f.read() # b'\xe4\xb8\xad\xe6\x96\x87'
3. 操作文件和目录
Python中os
模块的一些命令:
-
os.name
:获取操作系统类型os.name
是poisx
,系统是linux
、Unix
或Mac OS X
os.name
是nt
,系统是Windows
-
os.uname()
:获取更详细的信息使用- 但不支持
windows
系统。也就是说os
模块的某些函数是跟操作系统相关的
- 但不支持
-
os.path.abspath('xx')
:获取某一文件/路径的绝对路径 -
os.path.join('xx1', 'xx2')
:获取xx1
和xx2
的完整路径 -
os.path.mkdir('xxx')
:创建目录 -
os.getcwd()
:获取当前路径 -
os.chdir()
:切换路径 -
os.rename('xx1', 'xx2')
:对文件重命名 -
os.remove('xxx')
:删除文件 -
os
模块中没有复制文件的函数,因为复制文件并非由操作系统提供的系统调用- 解决:
shutil
模块的copyfile()
函数。shutil
模块可看做os
模块的补充
- 解决:
-
[x for x in os.listdir('xxx') if os.path.isdir(x)]
:过滤xxx文件夹- demo:在’C:\Users\Administrator\Desktop\ss\aa’下创建11.o、22.x两个文件夹
>>> [x for x in os.listdir('.') if os.path.isdir(x)] ['11.o', '22.x']
-
[x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.txt']
:获取当前目录下所有的.txt
文件- demo:在’C:\Users\Administrator\Desktop\ss\aa’下创建test1.txt、test2.txt两个TXT文件
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.txt'] ['test1.txt', 'test2.txt']
命令行输入操作系统提供的各种命令(比如dir
、cp
)可操作文件、目录。
Python的os
模块(导入:import os
)可直接调用操作系统提供的接口函数来执行文件、目录的操作。
1. 环境变量
os.environ
:获取操作系统定义的环境变量
os.environ.get('key')
:获取某个环境变量的值
2. 操作文件和目录
操作文件和目录的函数一部分放在os
模块中,一部分放在os.path
模块中。
查看、创建、删除目录(Windows为例,如果Linux/Mac路径’/Users/Jason/…’):
# 查看当前目录的绝对路径
>>> os.path.abspath('.')
'C:\\Users\\Administrator\\Desktop\\ss'
# 查看`text.txt`的绝对路径
>>> os.path.abspath('text.txt')
'C:\\Users\\Administrator\\Desktop\\ss\\text.txt'
# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来
>>> os.path.join('C:\\Users\\Administrator\\Desktop\\ss', 'aa')
'C:\\Users\\Administrator\\Desktop\\ss\\aa'
# 然后创建一个目录
>>> os.mkdir('C:\\Users\\Administrator\\Desktop\\ss\\aa')
# 删除一个目录
>>> os.rmdir('C:\\Users\\Administrator\\Desktop\\ss\\bb')
把两个路径合成一个时,不要直接拼接字符串,用os.path.join()
函数,这可正确处理不同操作系统的路径分隔符:
- Linux/Unix/Mac下,返回字符串
part-1/part-2
- Windows下返回
part-1\part-2
>>> os.path.join('C:\\Users\\Administrator\\Desktop\\ss\\aa','C:\\Users\\Administrator\\Desktop\\ee')
'C:\\Users\\Administrator\\Desktop\\ee'
拆分路径时,不要直接去拆字符串,用os.path.split()
函数,这样可把一个路径拆分成两部分,后一部分总是最后级别的目录或文件名:
>>> os.path.split('C:\\Users\\Administrator\\Desktop\\ss\\aa') # 目录
('C:\\Users\\Administrator\\Desktop\\ss', 'aa')
>>> os.path.split('C:\\Users\\Administrator\\Desktop\\ss\\aa\\test.txt') # 文件
('C:\\Users\\Administrator\\Desktop\\ss\\aa', 'test.txt')
os.path.splitext()
可直接获得文件扩展名:
>>> os.path.splitext('C:\\Users\\Administrator\\Desktop\\ss\\aa\\test.txt')
('C:\\Users\\Administrator\\Desktop\\ss\\aa\\test', '.txt')
这些合并、拆分路径的函数并不要求目录和文件要真实存在,他们只对字符串进行操作。
小结:
Python的os
模块封装了操作系统的目录和文件操作,要注意这些函数有的在os
模块,有的在os.path
模块。
4. 序列化
在程序运行的过程中,所有变量都在内存中。
比如定义一个dict:d = {'name': 'Bob', 'age': 25}
,可随时修改变量,比如把name
改为Jason
,但程序结束变量所占用的内存就被操作系统全部回收。如果没有把修改后的Jason
存储到磁盘上,下次重新运行程序,变量又被初始化为Bob
。
序列化:把变量从内存中变成可存储或传输的过程 (Python中叫pickling)
序列化后,就可以把系列化后的内容写入磁盘,或通过网络传输到别的机器上。
反序列化:把变量内容从序列化的对象重新读到内存中 (Python中叫unpickling)
Python提供了pickle
模块实现序列化。
demo:把一个对象序列化并写入文件
>>> import pickle
>>> d = {'name': 'Jason', 'age': 25}
>>> pickle.dumps(d)
b'\x80\x04\x95\x1c\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\x05Jason\x94\x8c\x03age\x94K\x19u.'
pickle.dumps()
方法:把任意对象序列化成一个bytes
,然后就可以把这个bytes
写入文件
pickle.dump()
方法:直接把对象序列化后写入一个file-like Object
:
>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()
会发现dump.txt
文件里一堆乱码,这些都是Python保存的对象内部信息。
把对象从磁盘读到内存时,可先把内容读到一个bytes
,然后用pickle.loads()
方法反序列出对象,或用pickle.load()
方法从一个file-like Object
中直接反序列化出对象:
>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'name': 'Jason', 'age': 25}
变量内容转换回来了,但这个变量d
跟之前的变量d
毫不相干,只是内容相同。
Pickle的问题和其他所有编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功的反序列化也没关系。
JSON
要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON。
JSON优势:
- JSON表示出来就是一个字符串,可以被所有语言读取
- 方便存储到磁盘或通过网络传输
- JSON不仅是标准格式,并且比XML更快
- 可直接在Web页面上读取,非常方便。
JSON表示的对象是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
---|---|
{} | dict |
[] | list |
“String” | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置的json
模块提供了非常完善的Python对象到JSON格式的转换。
序列化:
dumps()
方法:把python对象变成一个JSON,返回一个str
,内容就是标准的JSONdump()
方法:直接把JSON写入一个file-like Object
反序列化:
loads()
方法:把JSON的字符串反序列化load()
方法:从file-like Object
中读取字符串并反序列化
demo1:把python对象变成一个JSON
>>> import json
>>> d = {'name': 'Jason', 'age': 25}
>>> json.dumps(d)
'{"name": "Jason", "age": 25}'
demo2:把JSON反序列化为Python对象
>>> json_str = '{"name": "Bob", "age": 25}'
>>> json.loads(json_str)
{'name': 'Bob', 'age': 25}
由于JSON标准规定JSON编码是utf-8,所以我们总是能正确的在Python的str
与JSON的字符串之间转换。
JSON进阶
Python的dict
对象可直接序列化为JSON的{}
,不过很多时候用class
表示对象,比如定义Student
类,然后序列化:
import json
def student2dict(stu):
return {
'name': stu.name,
'age': stu.age,
'score': stu.score
}
class Student(object):
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
s = Student('Bob', 26, 98)
# print(json.dumps(s)) # s是一个Student实例对象不是可序列化为JSON的对象
# TypeError: Object of type Student is not JSON serializable
# Student对象不是一个可序列化为JSON的对象
print(json.dumps(s, default=student2dict))
# default 是将Student实例先被student2dict()函数转换成dict,然后再被序列化为JSON
# {"name": "Bob", "age": 26, "score": 98}
下次遇到一个Teacher
类的实例照样无法序列化为JSON。解决:把任意class
的实例变为dict
:
json.dumps(s, default=lambda obj: obj.dict)
因为通常class
的实例都有一个__dict__
属性,它就是一个dict
,用来存储实例变量。也有少数除外,比如定义了__slots__
的class。
把JSON反序列化为一个Student
对象实例,loads()
方法首先转换出一个dict
对象,然后,object_hook
函数把dict
转换为Student
实例:
def dict2student(d):
return Student(d['name'], d['age'], d['score'])
json_str = '{"name": "Jason", "age": 25, "score": 89}'
print(json.loads(json_str, object_hook=dict2student))
# 打印的是反序列化的Student实例对象
# <__main__.Student object at 0x000001E4B9A4AFD0>
小结:
- Python语言特定的序列化模块是
pickle
,但把序列化做的更通用、更符合Web标准,使用json
模块 json
模块的dumps()
和loads()
函数,使用时只需传入一个必须的参数。当默认的序列化或反序列化机制不满足要求时则传入更多参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性