IO編程
IO在計算機中指Input/Output,也就是輸入和輸出。由於程序和運行時數據是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方,通常是磁盤、網絡等,就需要IO接口。從磁盤讀取文件到內存,就只有Input操作,反過來,把數據寫到磁盤文件裏,就只是一個Output操作。由於CPU和內存的速度遠遠高於外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數據寫入磁盤,CPU輸出100M的數據只需要0.01秒,可是磁盤要接收這100M數據可能需要10秒,怎麼辦呢?有兩種辦法:
第一種是CPU等着,也就是程序暫停執行後續代碼,等100M的數據在10秒後寫入磁盤,再接着往下執行,這種模式稱爲同步IO;
另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不着急,我接着幹別的事去了”,於是,後續代碼可以立刻接着執行,這種模式稱爲異步IO。
同步和異步的區別就在於是否等待IO執行的結果。異步IO來編寫程序性能會遠遠高於同步IO,但是異步IO的缺點是編程模型複雜
一、文件讀寫
讀寫文件是最常見的IO操作。Python內置了讀寫文件的函數,用法和C是兼容的。讀寫文件就是請求操作系統打開一個文件對象(通常稱爲文件描述符),然後,通過操作系統提供的接口從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。1.從文件中讀取數據
1.1讀取整個文件
#pi_digits.txt
3.1415926535
8979323846
2643383279
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
函數open() 接受一個參數:要打開的文件的名稱。Python在當前執行的文件所在的目錄中查找指定的文件,函數open() 返回一個表示文件的對象。在這裏,open('pi_digits.txt') 返回一個表示文件pi_digits.txt 的對象;Python將這個對象存儲在我們將在後面使用的變量中。
關鍵字with 在不再需要訪問文件後將其關閉。
PS:在這個程序中,注意到我們調用了open() ,但沒有調用close() ;調用open() 和close() 來打開和關閉文件,如果程序存在bug,導致close() 語句未執行,文件將不會關閉。未妥善地關閉文件可能會導致數據丟失或受損。如果在程序中過早地調用close() ,需要使用文件時它已關閉(無法訪問),會導致更多的錯誤。通過使用前面所示的結構,可讓Python去確定:你只管打開文件,並在需要時使用它,Python自會在合適的時候自動將其關閉。
函數read() 讀取這個文件的全部內容,並將其作爲一個長長的字符串存儲在變量contents中。這樣,通過打印contents 的值,就可將這個文本文件的全部內容顯示出來。相比於原始文件,該輸出不同的地方是末尾多了一個空行。read() 到達文件末尾時返回一個空字符串,而將這個空字符串顯示出來時就是一個空行。要刪除多出來的空行,可在print 語句中使用rstrip() :
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents.rstrip())
調用read()
會一次性讀取文件的全部內容,如果文件有10G,內存就爆了,所以,要保險起見,可以反覆調用read(size)
方法,每次最多讀取size個字節的內容。另外,調用readline()
可以每次讀取一行內容,調用readlines()
一次讀取所有內容並按行返回list
。因此,要根據需要決定怎麼調用。
如果文件很小,read()
一次性讀取最方便;如果不能確定文件大小,反覆調用read(size)
比較保險;如果是配置文件,調用readlines()
最方便:
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'刪掉
file-like Object:像open()
函數返回的這種有個read()
方法的對象,在Python中統稱爲file-like Object。除了file外,還可以是內存的字節流,網絡流,自定義流等等。file-like Object不要求從特定類繼承,只要寫個read()
方法就行。StringIO
就是在內存中創建的file-like Object,常用作臨時緩衝。
二進制文件:前面講的默認都是讀取文本文件,並且是UTF-8編碼的文本文件。要讀取二進制文件,比如圖片、視頻等等,用'rb'
模式打開文件即可:
>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六進制表示的字節
字符編碼:要讀取非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')
1.2文件路徑
with open('text_files/filename.txt') as file_object:
在Windows系統中,在文件路徑中使用反斜槓(\ )而不是斜槓(/ ):with open('text_files\filename.txt') as file_object:
在相對文件路徑行不通時,可使用絕對文件路徑,絕對路徑通常比相對路徑更長,因此將其存儲在一個變量中,再將該變量傳遞給open() 會有所幫助。在Linux和OS X中,絕對路徑類似於下面這樣:
file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
在Windows系統中,它們類似於下面這樣:
file_path = 'C:\Users\ehmatthes\other_files\text_files\filename.txt'
with open(file_path) as file_object:
通過使用絕對路徑,可讀取系統任何地方的文件。就目前而言,最簡單的做法是,要麼將數據文件存儲在程序文件所在的目錄,要麼將其存儲在程序文件所在目錄下的一個文件夾(如text_files)中。
1.3逐行讀取
❶ filename = 'pi_digits.txt'
❷ with open(filename) as file_object:
❸ for line in file_object:
print(line)
在這個文件中,每行的末尾都有一個看不見的換行符,而print 語句也會加上一個換行符,因此每行末尾都有兩個換行符:一個來自文件,另一個來自print 語句。
3.1415926535
8979323846
2643383279
要消除這些多餘的空白行,可在print 語句中使用rstrip() :
filename = 'pi_digits.txt'
with open(filename) as file_object:
for line in file_object:
print(line.rstrip())
1.4創建一個包含文件各行內容的列表
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())
1.5使用文件內容
將文件讀取到內存中後,就可以以任何方式使用這些數據了。下面以簡單的方式使用圓周率的值。首先,我們將創建一個字符串,它包含文件中存儲的所有數字,且沒有任何空格:filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip() #變量pi_string存儲的字符串中,包含原來位於每行左邊的空格,爲刪除這些空格,可使用strip()
print(pi_string)
print(len(pi_string))
注意讀取文本文件時,Python將其中的所有文本都解讀爲字符串。如果你讀取的是數字,並要將其作爲數值使用,就必須使用函數int() 將其轉換爲整數,或使用函數float() 將其轉換爲浮點數。
1.6包含一百萬位的大型文件
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip()
print(pi_string[:52] + "...")
print(len(pi_string))
birthday = input("Enter your birthday, in the form mmddyy: ")
if birthday in pi_string:
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appear in the first million digits of pi.")
2.寫入文件
保存數據的最簡單的方式之一是將其寫入到文件中 2.1寫入空文件
要將文本寫入文件,你在調用open() 時需要提供另一個實參,告訴Python你要寫入打開的文件。傳入標識符'w'
或者'wb'
表示寫文本文件或寫二進制文件:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.\n")
file_object.write("I love creating new games.")#像顯示到終端的輸出一樣,還可以使用空格、製表符和空行來設置這些輸出的格式。
第一個實參也是要打開的文件的名稱;第二個實參('w' )告訴Python,我們要以寫入模式 打開這個文件。打開文件時,可指定讀取模式 ('r' )、寫入模式 ('w' )、附加模式 ('a' )或讓你能夠讀取和寫入文件的模式('r+' )。如果省略了模式實參,Python將以默認的只讀模式打開文件。
2.2附加到文件
filename = 'programming.txt'
with open(filename, 'a') as file_object:
file_object.write("I also love finding meaning in large datasets.\n")
file_object.write("I love creating apps that can run in a browser.\n")
二、StringIO和BytesIO
StringIO
很多時候,數據讀寫不一定是文件,也可以在內存中讀寫。StringIO顧名思義就是在內存中讀寫str。要把str寫入StringIO,我們需要先創建一個StringIO,然後,像文件一樣寫入即可:
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hello world!
getvalue()
方法用於獲得寫入後的str。
要讀取StringIO,可以用一個str初始化StringIO,然後,像讀文件一樣讀取:
>>> from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
... s = f.readline()
... if s == '':
... break
... print(s.strip())
...
Hello!
Hi!
Goodbye!
BytesIO
StringIO操作的只能是str,如果要操作二進制數據,就需要使用BytesIO。BytesIO實現了在內存中讀寫bytes,我們創建一個BytesIO,然後寫入一些bytes: 寫入的不是str,而是經過UTF-8編碼的bytes。
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
和StringIO類似,可以用一個bytes初始化BytesIO,然後,像讀文件一樣讀取:>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'
小結:StringIO和BytesIO是在內存中操作str和bytes的方法,使得和讀寫文件具有一致的接口。
三、操作文件和目錄
操作文件和目錄的函數一部分放在os
模塊中,一部分放在os.path
模塊中,這一點要注意一下。查看、創建和刪除目錄可以這麼調用:
# 查看當前目錄的絕對路徑:
>>> os.path.abspath('.')
'/Users/michael'
# 在某個目錄下創建一個新目錄,首先把新目錄的完整路徑表示出來:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'
# 然後創建一個目錄:
>>> os.mkdir('/Users/michael/testdir')
# 刪掉一個目錄:
>>> os.rmdir('/Users/michael/testdir')
把兩個路徑合成一個時,不要直接拼字符串,而要通過os.path.join()
函數,這樣可以正確處理不同操作系統的路徑分隔符。在Linux/Unix/Mac下,os.path.join()
返回這樣的字符串:part-1/part-2
Windows下會返回這樣的字符串:part-1\part-2
同樣的道理,要拆分路徑時,也不要直接去拆字符串,而要通過os.path.split()
函數,這樣可以把一個路徑拆分爲兩部分,後一部分總是最後級別的目錄或文件名:>>> os.path.split('/Users/michael/testdir/file.txt')
('/Users/michael/testdir', 'file.txt')
os.path.splitext()
可以直接讓你得到文件擴展名,很多時候非常方便:>>> os.path.splitext('/path/to/file.txt')
('/path/to/file', '.txt')
這些合併、拆分路徑的函數並不要求目錄和文件要真實存在,它們只對字符串進行操作。文件操作使用下面的函數。假定當前目錄下有一個test.txt
文件:
# 對文件重命名:
>>> os.rename('test.txt', 'test.py')
# 刪掉文件:
>>> os.remove('test.py')
複製文件的函數在os
模塊中不存在!原因是複製文件並非由操作系統提供的系統調用。但是shutil
模塊提供了copyfile()
的函數,你還可以在shutil
模塊中找到很多實用函數,它們可以看做是os
模塊的補充。
>>> [x for x in os.listdir('.') if os.path.isdir(x)]
['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]
列出所有的.py
文件,也只需一行代碼:>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']
['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']
四、序列化 變量從內存中變成可存儲或傳輸的過程稱之爲序列化,在Python中叫pickling,序列化之後,就可以把序列化後的內容寫入磁盤,或者通過網絡傳輸到別的機器上。反過來,把變量內容從序列化的對象重新讀到內存裏稱之爲反序列化,即unpickling。Python提供了pickle
模塊來實現序列化。
把一個對象序列化並寫入文件:
>>> import pickle
>>> d = dict(name='Bob', age=20, score=88)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'
>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()
把對象從磁盤讀到內存時,可以先把內容讀到一個bytes
,然後用pickle.loads()
方法反序列化出對象>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}
1、JSON
如果要在不同的編程語言之間傳遞對象,就必須把對象序列化爲標準格式,比如XML,但更好的方法是序列化爲JSON,因爲JSON表示出來就是一個字符串,可以被所有語言讀取,也可以方便地存儲到磁盤或者通過網絡傳輸。JSON不僅是標準格式,並且比XML更快,而且可以直接在Web頁面中讀取,非常方便。 Python內置的json
模塊提供了非常完善的Python對象到JSON格式的轉換
JSON表示的對象就是標準的JavaScript語言的對象,JSON和Python內置的數據類型對應如下:
JSON | Python |
{} | dict |
[] | list |
"string" | str |
1234.56 | int或float |
true/false | True/False |
null | None |
1.1使用模塊json 來存儲數據
1.1.1使用json.dump() 和json.load()
json
模塊提供了非常完善的Python對象到JSON格式的轉換。我們先看看如何把Python對象變成一個JSON,>>> import json
>>> d = dict(name='Bob', age=20, score=88)
>>> json.dumps(d)
'{"age": 20, "score": 88, "name": "Bob"}' #dumps()方法返回一個str,內容就是標準的JSON
import json
numbers = [2, 3, 5, 7, 11, 13]
filename = 'numbers.json'
with open(filename, 'w') as f_obj:
json.dump(numbers, f_obj)
json.dumps()
提供了一個ensure_ascii
參數import json
obj = dict(name='小明', age=20)
s = json.dumps(obj, ensure_ascii=False)
print(s)
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
>>> json.loads(json_str)
{'age': 20, 'score': 88, 'name': 'Bob'}
import json
filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)
print(numbers)
1.2保存和讀取用戶生成的數據
import json
# 如果以前存儲了用戶名,就加載它
# 否則,就提示用戶輸入用戶名並存儲它
filename = 'username.json'
try:
❶ with open(filename) as f_obj: #嘗試打開文件username.json。如果這個文件存在,就將其中的用戶名讀取到內存中(見❷)
❷ username = json.load(f_obj)
❸ except FileNotFoundError: #用戶首次運行這個程序時,文件username.json不存在,將引發FileNotFoundError 異常
❹ username = input("What is your name? ")
❺ with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")
無論執行的是except 代碼塊還是else 代碼塊,都將顯示用戶名和合適的問候語。如果這個程序是首次運行,輸出將如下:What is your name? Eric
We'll remember you when you come back, Eric!
否則,輸出將如下:Welcome back, Eric!
1.3重構
要重構1.2代碼,可將其大部分邏輯放到一個或多個函數中。1.2代碼的重點是問候用戶,因此我們將其所有代碼都放到一個名爲greet_user() 的函數中:
import json
def greet_user():
❶ """問候用戶,並指出其名字"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")
greet_user()
這個程序更清晰些,但函數greet_user() 所做的不僅僅是問候用戶,還在存儲了用戶名時獲取它,而在沒有存儲用戶名時提示用戶輸入一個。下面來重構greet_user() ,讓它不執行這麼多任務。爲此,我們首先將獲取存儲的用戶名的代碼移到另一個函數中:
import json
def get_stored_username():
❶ """如果存儲了用戶名,就獲取它"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
❷ return None
else:
return username
def greet_user():
"""問候用戶,並指出其名字"""
username = get_stored_username()
❸ if username:
print("Welcome back, " + username + "!")
else:
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
greet_user()
新增的函數get_stored_username() 目標明確,❶處的文檔字符串指出了這一點。如果存儲了用戶名,這個函數就獲取並返回它;如果文件username.json不存在,這個函數就返回None (見❷)。這是一種不錯的做法:函數要麼返回預期的值,要麼返回None ;這讓我們能夠使用函數的返回值做簡單測試。在❸處,如果成功地獲取了用戶名,就打印一條歡迎用戶回來的消息,否則就提示用戶輸入用戶名。我們還需將greet_user() 中的另一個代碼塊提取出來:將沒有存儲用戶名時提示用戶輸入的代碼放在一個獨立的函數中:
import json
def get_stored_username():
"""如果存儲了用戶名,就獲取它"""
--snip--
def get_new_username():
"""提示用戶輸入用戶名"""
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
return username
def greet_user():
"""問候用戶,並指出其名字"""
username = get_stored_username()
if username:
print("Welcome back, " + username + "!")
else:
username = get_new_username()
print("We'll remember you when you come back, " + username + "!")
greet_user()
這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user() ,它打印一條合適的消息:要麼歡迎老用戶回來,要麼問候新用戶。爲此,它首先調用get_stored_username() ,這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username() ,這個函數只負責獲取並存儲新用戶的用戶名。要編寫出清晰而易於維護和擴展的代碼,這種劃分工作必不可少。