前言:由於程序和運行數據是在內存中駐留的,由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()函數是定義得非常好的接口的典范。當我們使用時,只需要傳入一個必須的參數。但是,當默認的序列化或反序列機制不滿足我們的要求時,我們又可以傳入更多的參數來定制序列化或反序列化的規則,既做到了接口簡單易用,又做到了充分的擴展性和靈活性。