Python之IO编程
前言:由于程序和运行数据是在内存中驻留的,由CPU这个超快的计算核心来执行。当涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。由于CPU和内存的速度远远高于外设的速度,那么在IO编程中就存在速度严重不匹配的问题。这时有2种解决办法,一是同步IO(CPU暂停直到数据重新写入完到磁盘中)二是,异步IO(CPU不等待,继续执行后续代码)。明显异步的复杂度高于同步IO,所以在这里只讨论同步的IO。
参考原文
文件读写
我们都使用过文件读写,应该知道读写文件是最常见的IO操作。Python也内置了读写文件的函数,用法是和C兼容的。注意在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘。所以,读写文件就是请求操作系统打开一个文件对象(文件描述符),然后通过操作系统提供的接口从这个文件对象中读取或写入文件。
读取文件
我们要读取一个txt文件的内容,可以使用Python内置的open()函数,以读文件的模式打开一个文件对象,并传入文件名和标识符。如:
f = open(r'G:/Python/dump.txt', 'r')
标识符‘r’表示可读,接下来就可以读取文件的内容了:
>>> f.read() 'Hello,world!'
如果要读取二进制文件,比如图片、视频等,就需要用'rb'模式打开文件:
>>> with open(r'G:/python/image/preview.jpg', 'rb') as f: print(f.readline()) b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00x\...
如果要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如读取GBK编码的文件:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk') >>> f.read() '测试'
遇到有些编码不规范 的文件,你可能会遇到UnicodeDecodeError,遇到这种情况,open()函数还接收一个errors参数,表示遇到编码错误后怎么处理,最简单是忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')
切记最后一步必须关闭文件,因为文件对象会占用操作系统的资源,并且操作系统统一时间能打开的文件数量也是有限的,可以通过close()方法来关闭文件,但是我们知道文件读写过程中可能会出错,一旦出错后面的f.close()就不会被调用了,所以为了防止,可以使用try ... finally ,但是太麻烦了,所以,Python引入了with语句来帮我们自动调用close()方法。
with open('/path/to/file', 'r') as f: print(f.read())
调用read()方法会一次读取所有的内容,如果文件有10G,内存就爆了,所以保险起见,可以反复调用read(size)方法,也可以调用readline()每次读取一行的内容。
Tips:如果文件很小,使用read()一次性读取最方便;如果不能确定文件的大小,反复调用read(size)是最好的;如果是配置文件,调用readlines()最方便。
file-like Object
只有对象有个read()方法,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。StringIO就是在内存中创建的file-like Object,常用作临时缓冲。
写入文件
前面已经说过怎么读文件了,其实写文件和读文件是一样的,唯一的区别,在于标识符变成‘w’或‘wb’以分别表示写文本文件和二进制文件,注意还是尽量用with语句来避免出错(如未写完):
with open('/Users/michael/test.txt', 'w') as f: f.write('Hello, world!')
Tips:以‘w’模式写入文件,相当于是覆盖文件。以‘a’模式写入文件,相当于是追加到文件的末尾。
StringIO和BytesIO
上面所说的都是读写文件在磁盘中,下面将说的是在内存中读写。
StringIO
顾名思义,就是在内存中读写str。要将str写入StringIO,创建一个StringIO,然后便可像文件一样写入,getvalue()方法用于获得写入后的str。
>>> from io import StringIO >>> f = StringIO() >>> f.write('hello') 5 >>> f.write(' world!') 7 >>> print(f.getvalue()) hello world!
StringIO也可以像文件一样读取,先用str初始化StringIO:
>>> from io import StringIO >>> f = StringIO('Hello\n,world') >>> while True: s = f.readline() if s == '': break print(s) Hello ,world
BytesIO
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO了。使用如下:
>>> from io import BytesIO >>> f = BytesIO() >>> f.write('中文'.encode('utf-8')) 6 >>> print(f.getvalue()) b'\xe4\xb8\xad\xe6\x96\x87'
注意:写入的不是str,而是经过UTF-8编码的bytes。也可以用一个bytes初始化BytesIO,然后像文件一样读取:
>>> from io import BytesIO >>> f = BytesIO('中文'.encode('utf-8')) >>> f.read() b'\xe4\xb8\xad\xe6\x96\x87'
Tips:StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
操作文件和目录
上面所说的都是操作文件内容的方法,我们知道,有时我们不得不操作文件本身和目录。那么在Python程序中执行文件和目录的操作该怎么办呢?其实操作系统提供的命令只是简单地调用了操作系统提供的接口函数,Python内置的os模块也可以直接调用操作系统提供的接口函数。
打开Python交互式命令行,来看看如何使用os模块的基本功能:
>>> import os >>> os.name #操作系统类型 'nt'
如果os.name 是posix,则说明系统是Linux、Unix或Mac OS X,如果是nt就是windows系统。
在Linux下,可以使用uname()函数来获取详细的系统信息(widows没有uname函数):
>>> os.uname() posix.uname_result(sysname='Darwin', nodename='MichaelMacPro.local', release='14.3.0', version='Darwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64', machine='x86_64')
再来说一下操作系统中定义的环境变量,全部保存在os.environ这个变量中,可以直接查看:
>>> os.environ environ({'ALLUSERSPROFILE': 'C:\\ProgramData', 'APPDATA': 'C:\\Users\\Administrator\\AppData\\Roaming', 'COMMONPROGRAMFILES': 'C:\\Program Files\\Common Files', 'COMMONPROGRAMFILES(X86)': 'C:\\Program Files (x86)\\Common Files', 'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files', 'COMPUTERNAME': 'LICHEN', 'COMSPEC': 'C:\\WINDOWS\\system32\\cmd.exe', '...
也可以通过os.environ.get('key')来获取某个环境变量的值:
>>> os.environ.get('PATH') 'C:\\WINDOWS\\system32;C:\\WINDOWS;C:\\WINDOWS\\System32\\Wbem;C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\;C:\\Program Files (x86)\\Microsoft SQL Server\\Client SDK\\ODBC\\130\\Tools\\Binn\\;C:\\Program Files (x86)\\Microsoft SQL Server\\140\\Tools\\Binn\\;C:\\Program Files (x86)\\Microsoft SQL Server\\140\\DTS\\Binn\\;C:\\Program Files (x86)\\Microsoft SQL Server\\140\\Tools\\Binn\\ManagementStudio\\;C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\130\\Tools\\Binn\\;C:\\Program Files (x86)\\Microsoft SQL Server\\130\\Tools\\Binn\\;C:\\Program Files\\Microsoft SQL Server\\130\\Tools\\Binn\\;C:\\Program Files\\Microsoft SQL Server\\130\\DTS\\Binn\\;C:\\Program Files\\dotnet\\;C:\\Program Files\\Redis\\;F:\\local\\bin;F:\\Program Files\\Git\\cmd;F:\\Anaconda3;F:\\Anaconda3\\Library\\mingw-w64\\bin;F:\\Anaconda3\\Library\\usr\\bin;F:\\Anaconda3\\Library\\bin;F:\\Anaconda3\\Scripts;C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python36\\Scripts\\;C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python36\\;C:\\Users\\Administrator\\AppData\\Local\\Microsoft\\WindowsApps;F:\\Program Files\\Microsoft VS Code Insiders\\bin'
除了os模块中,操作文件和目录的函数另一部分在os.path模块中。查看、创建、删除目录可以这么调用:
# 查看当前目录的绝对路径 >>> os.path.abspath('.') 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python36' #在某个目录下下创建一个新目录,首先把新目录的完整路径表示出来 >>> path = os.path.join(r'G:/python/', 'new') # 然后在创建一个目录 >>> os.mkdir(path) #再删掉该目录 >>> os.rmdir(path)
os.path.splitext()可以让你得到文件的扩展名:
>>> os.path.splitext('/path/to/file.txt') ('/path/to/file', '.txt')
Tips:注意把两个路径合成一个时,不要直接拼接字符串,而要通过os.path.join()函数,拆分路径时使用os.path.split()函数。这些合并。拆分路径的函数并不 要求目录和文件真实存在,它们只对字符串进行操作。
文件操作使用下面的函数,假定当前目录有一个test.txt文件:
# 对文件重命名: >>> os.rename('test.txt', 'test.py') # 删掉文件: >>> os.remove('test.py')
好像没有复制文件的函数在os模块中!原因是复制文件并非由操作系统提供的系统调用。幸运的是shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
最后看看怎么利用Python的特性来过滤文件,比如我们要列出当前目录下的所有目录:
>>> [x for x in os.listdir('.') if os.path.isdir(x)] ['DLLs', 'Doc', 'include', 'Lib', 'libs', 'Scripts', 'tcl', 'Tools', '__pycache__']
再列出所有的.py文件:
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] ['hello.py']
Tips:Python的os模块封装了操作系统的目录和文件操作,要注意这些函数有的在os模块中,有的在os.path模块中。
序列化
我们把变量从内存中变成可存储或传输的过程称为序列化,在Python中叫pickling,在其他语言中也被称为serialization,marshalling,flattening。
为什么要序列化?序列化后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量的内容从序列化的对象中重新读到内存里就称之为反序列化,即unpickling。
来看看在Python中怎么用?Python提供了模块pickle来实现序列化。我们先尝试把一个对象序列化写入文件:
>>> import pickle >>> d = dict(name='Bob', age=20, score=88) >>> pickle.dumps(d) b'\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x03\x00\x00\x00Bobq\x02X\x03\x00\x00\x00ageq\x03K\x14X\x05\x00\x00\x00scoreq\x04KXu.' >>> content = pickle.dumps(d) >>> with open(r'G:/python/dump.txt', 'wb') as f: f.write(content) 55
我们可以用pickle.dumps()方法把任意对象序列化成一个bytes,然后把这个bytes写入文件中。或者用另外一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:
>>> with open(r'G:/python/dump.txt', 'wb') as f: pickle.dump(d, f)
那又怎么反序列化呢?当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes里,然后用pickle.loads()方法反序列化对象,也可以直接用pickle.load()方法直接从一个file-like Object中直接反序列化出对象。
>>> import pickle >>> with open(r'G:/python/dump.txt', 'rb') as f: obj = f.read() >>> d = pickle.loads(obj) >>> d {'name': 'Bob', 'age': 20, 'score': 88} >>> with open(r'G:/python/dump.txt', 'rb') as f: d = pickle.load(f) >>> d {'name': 'Bob', 'age': 20, 'score': 88}
但是Pickle是Python特有的,只能用于Python,并且可能不同的版本都不兼容,很有可能不能成功的反序列化一些对象(其他语言写的),所以为了解决这个问题,来看看Json。
JSON
什么是JSON?Json是一个标准的JavasScript对象,JSON表示出来的其实就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。
JSON的优点?可以用在不同的编程语言中传递对象,而且是标准格式比XML更快,也可以在web页面中读取,非常方便。
JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
---|---|
{} | dict |
[] | list |
"string" | str |
1234.56 | int或float |
true/false | True/False |
null | None |
来看看Python对象与JSON的互相转换吧,我们先看下如何把Python对象变成一个JSON吧:
>>> import json >>> d = dict(name='Bob', age=20, score=88) >>> json.dumps(d) '{"name": "Bob", "age": 20, "score": 88}'
json.dumps()方法返回一个str,内容就是标准的JSON。与pickle类似,json.dump()可以直接把JSON写入一个file-like Objec。
要把JSON反序列化为Python对象,用json.loads()或者对应的json.load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:
>>> json_str = '{"age":20, "score":88, "name":"Bob"}' >>> json.loads(json_str) {'age': 20, 'score': 88, 'name': 'Bob'}
Json进阶
虽然Python中的dict对象可以直接序列化为JSON中的{},不过在很多时候,想必我们更喜欢用class表示对象,然后再序列化。比如定义一个Student类,然后序列化它的一个实例:
import json class Student(object): def __init__(self, name, age, score): self.name = name self.age = age self.score= score def student2dict(std): return { 'name': std.name, 'age': std.age, 'score': std.score } s = Student('Bob', 20, 88) print(json.dumps(s, default=student2dict)) #result {"name": "Bob", "age": 20, "score": 88}
与dict不同的是,此时需要加上可选参数default(把任意一个对象变成一个可序列化JSON的对象),default接收的是一个函数(怎么转化为JSON的对象),这样Student实例首先被student2dict()函数转换成dict,然后再被顺利地转为JSON。
这样似乎有点麻烦,我们可以偷个懒,把任意class的实例变为dict,因为通常的实例都有一个__dict__属性,用来存储实例变量。也有少数例外,如定义了__slots__的class。所以可以这样:
print(json.dumps(s, default=lambda obj: obj.__dict__)) #result {"name": "Bob", "age": 20, "score": 88}
那怎么将JSON反序列化为对象实例呢?可以用json.loads()方法首先得到一个dict对象,然后我们传入的object__hook函数负责把dict转换为Student实例:
def dict2student(d): return Student(d['name'], d['age'], d['score']) json_str = '{"age": 20, "score": 88, "name": "Bob"}' print(json.loads(json_str, object_hook=dict2student)) #result <__main__.Student object at 0x000002085FDE0438>
Tips:Python中语言特定的序列化模块时pickle,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json模块。json模块的dumps()和loads()函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。