第十章(二)
2、寫入文件
保存數(shù)據(jù)的最簡單的方式之一是將其寫入到文件中。
(1)寫入空文件
要將文本寫入文件,需要在調(diào)用open()時提供另一個參數(shù),告訴Python你要寫入打開的文件。
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.")
函數(shù)open()有兩個實參,第一個是要打開的文件的名稱;第二個是告訴Python,我們要以寫入模式打開這個文件。打開文件時,可指定讀取模式('r')、寫入模式('w')、附加模式('a')或讓你能夠讀取和寫入文件的模式('r+')。如果你省略了模式實參,Python將以默認的只讀模式打開文件。
如果你要寫入的文件不存在,函數(shù)open()將自動創(chuàng)建它。然而,以寫入('w')模式打開文件時千萬要小心,因為如果指定的文件已經(jīng)存在,Python將在返回文件對象前清空該文件,即先清空文件再執(zhí)行寫入操作。
注意:Python只能將字符串寫入文本文件。要將數(shù)值數(shù)據(jù)存儲到文本文件中,必須先使用函數(shù)str()將其轉(zhuǎn)換為字符串格式。
(2)寫入多行
函數(shù)write()不會在你寫入的文本末尾添加換行符。要讓每個字符串都單獨占一行,需要在write()語句中包含換行符:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love create new games.\n")
file_object.write("I love programming.\n")
# 文件中顯示為:
I love create new games.
I love programming.
可以使用空格、制表符和空行來設(shè)置這些輸出的格式。
(3)附加到文件
如果你要給文件添加內(nèi)容,而不是覆蓋原有的內(nèi)容,可以附加模式打開文件。這時,Python不會在返回文件對象前清空文件,而你寫入到文件的行都將添加到文件末尾。如果指定的文件不存在,Python將會創(chuàng)建一個空文件。
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 create apps that can run in a browser.\n")
# 文件中顯示為:
I love create new games.
I love programming.
I also love finding meaning in large datasets.
I love create apps that can run in a browser.
3、異常
Python使用被稱為異常的特殊對象來管理程序執(zhí)行期間發(fā)生的錯誤。每當發(fā)生讓Python不知所措的錯誤時,它都會創(chuàng)建一個異常對象。如果你編寫了處理該異常的代碼,程序?qū)⒗^續(xù)執(zhí)行;如果你未對異常進行處理,程序?qū)⑼V梗@示一個traceback,其中包含有關(guān)異常的報告。
異常是使用try-except代碼塊處理的。try-except代碼塊讓Python執(zhí)行指定的操作,同時告訴Python發(fā)生異常時怎么辦。使用了try-except代碼塊,即便出現(xiàn)異常,程序也將繼續(xù)運行:顯示你編寫的友好的錯誤信息,而不是令人迷惑的traceback。
(1)處理ZeroDivisionError異常
將一個數(shù)字除以0引發(fā)異常錯誤:
print(5/0)
# 報錯提示:
ZeroDivisionError: division by zero
ZeroDivisionError是一個異常對象,在這種情況下,Python將停止運行程序,并指出引發(fā)了哪種異常。
(2)使用try-except代碼塊
當你認為可能發(fā)生了錯誤時,可編寫一個try-except代碼塊來處理可能引發(fā)的異常。處理ZeroDivisionError異常的try-except代碼塊:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
# 輸出:
You can't divide by zero!
如果try代碼塊中的代碼運行起來沒有問題,Python將跳過except代碼塊;如果try代碼塊中的代碼導(dǎo)致了錯誤,Python將查找這樣的except代碼塊,并運行其中的代碼。
如果try-except代碼塊后面還有其他代碼,程序?qū)⒔又\行,因為已經(jīng)告訴Python如何處理這種錯誤。
(3)使用異常避免崩潰
發(fā)生錯誤時,如果程序還有工作沒完成,妥善地處理I錯誤尤為重要。這種情況經(jīng)常會出現(xiàn)在要求用戶提供輸入的程序中;如果程序能夠妥善地處理無效輸入,就能在提示用戶提供有效的輸入,而不至于崩潰。下面是一個只執(zhí)行除法運算的簡單計算器:
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Secong number: ")
if second_number == 'q':
break
answer = int(first_number) / int(second_number)
print(answer)
這個程序沒有采取任何處理錯誤的措施,因此當除數(shù)為0時,它會崩潰。程序崩潰可不好,但讓用戶看到traceback也不是好主意。不懂技術(shù)的用戶會被它們搞糊涂,而且如果用戶懷有惡意,他會通過traceback獲悉你不希望他知道的信息,尤其是訓練有素的攻擊者可以根據(jù)這些信息判斷出可對你的代碼發(fā)起什么樣的攻擊。
(4)else代碼塊
通過將可能引發(fā)錯誤的代碼放在try-except代碼塊中,可提高這個程序的抵御錯誤的能力。錯誤是執(zhí)行除法運算的代碼行導(dǎo)致的,因此需要把該行代碼放在try-except中。其次,還添加了一個else代碼塊,依賴于try代碼塊成功執(zhí)行的代碼都應(yīng)放到else代碼塊中:
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Secong number: ")
if second_number == 'q':
break
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)
# 輸出:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Secong number: 0
You can't divide by 0!
First number: q
try-except-else代碼塊的工作原理大致為:Python嘗試執(zhí)行try代碼塊中的代碼;只有可能引發(fā)異常的代碼才需要放在try語句中。有時候,有一些代碼需要在try代碼塊成功運行后才運行,這些代碼應(yīng)放在else代碼塊中。except代碼塊告訴Python,如果它嘗試運行try代碼塊中的代碼時引發(fā)了指定的異常,該怎么辦。
通過預(yù)測可能發(fā)生錯誤的的代碼,可編寫健壯的程序,它們即便面臨無效數(shù)據(jù)或缺少資源,也能繼續(xù)運行,從而能夠抵御無意的用戶錯誤和惡意的攻擊。
(5)處理FileNotFoundError異常
使用文件時,當你找不到文件時,可使用try-except代碼塊以直觀的方式進行處理。下面嘗試讀取一個不存在的文件:
filename = 'alice.txt'
with open(filename) as f_obj:
contents = f_obj.read()
# 報告異常:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
這里引發(fā)了FileNotFoundError異常,這是Python找不到要打開的文件時創(chuàng)建的異常。這個錯誤是open()函數(shù)導(dǎo)致的,因此要將該函數(shù)放在try代碼塊中。
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
# 輸出:
Sorry, the file alice.txt does not exist.
(6)分析文本
你可以分析包含整本書的文本文件。項目Gutenberg(http://gutenberg.org/)提供了一系列不受版權(quán)限制的文學作品,如果你要在編程項目中使用文學文本,這是一個很不錯的資源。
下面來提取一小段文本,并計算它包含多少個單詞:
title = "Alice in Wonderland"
print(title.split())
# 輸出:
['Alice', 'in', 'Wonderland']
方法split()以空格為分隔符將字符串分拆成多個部分,并將這些部分都存儲到一個列表中,結(jié)果是一個包含字符串中所有單詞的列表,雖然有些單詞可能包含標點。
下面計算Alice in Wonderland童話中包含多少個單詞:
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
# 輸出:
The file alice.txt has about 29461 words.
我們把相應(yīng)的文件移到正確的目錄下,讓try代碼塊能夠成功執(zhí)行。
(7)使用多個文件
為了多分析幾個文本文件,可以將上述代碼的大部分移到一個函數(shù)中,這樣調(diào)用起來更方便。
def count_words(filename):
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
filename = 'alice.txt'
count_words(filename)
有了這個函數(shù),我們可以編寫一個簡單的循環(huán),計算要分析的任何文本包含多少個單詞。下面將要分析的文件存儲在一個列表中,并對列表調(diào)用上述的函數(shù)進行計算:
def count_words(filename):
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
# 輸出:
The file alice.txt has about 29461 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
注意:這里故意沒把siddhartha.txt放在相應(yīng)目錄中,我們可以看到即使文件不存在,也不影響這個程序處理其他存在的文件。另外還可以避免讓用戶看到traceback。
(8)失敗時一聲不吭
程序捕獲到異常時,如果你不希望將異常告訴用戶,而是想什么都沒發(fā)生一樣繼續(xù)運行,這時候?qū)xcept代碼塊中的所有語句用一個pass語句來代替就可以了,表示什么都不用做,讓程序順利運行。這時的輸出就不會有告訴你異常的那一句了,而其他的也能正常運行。
除此之外,pass語句還充當了占位符,它提醒你在程序的某個地方什么都沒做,并且以后也許要在這里做些什么。
(9)決定報告哪些錯誤
向用戶顯示他不想看到的信息可能會降低程序的可用性。Python的錯誤處理結(jié)構(gòu)讓你能夠細致地控制與用戶分享錯誤信息的程度,要分享多少信息由你決定。
編寫得很好且經(jīng)過詳盡測試的代碼不容易出現(xiàn)內(nèi)部錯誤,但只要程序依賴于外部因素,如用戶輸入、存在指定的文件、有網(wǎng)絡(luò)鏈接,就有可能出現(xiàn)異常。憑借經(jīng)驗可判斷該在程序的什么地方包含異常處理塊,以及出現(xiàn)錯誤時該向用戶提供多少相關(guān)的信息。
4、存儲數(shù)據(jù)
使用模塊json來存儲數(shù)據(jù)。模塊json讓你能夠?qū)⒑唵蔚腜ython數(shù)據(jù)結(jié)構(gòu)存儲到文件中,并在程序再次運行時加載該文件中的數(shù)據(jù)。你還可以使用json模塊在Python程序之間分享數(shù)據(jù)。更重要的是,JSON數(shù)據(jù)格式并非Python專用的,你可以以JSON格式存儲的數(shù)據(jù)與使用其他編程語言的人分享。JSON(JavaScript Object Notation)格式最初是為JavaScript開發(fā)的,但隨后成了一種常見格式,被包括Python在內(nèi)的眾多語言采用。
(1)使用json.dump()和json.load()
函數(shù)json.dump()接受兩個實參:要存儲的數(shù)據(jù)以及可用于存儲數(shù)據(jù)的文件對象。
import json
numbers = [2, 3, 5, 7, 9, 11]
filename = 'numbers.json'
with open(filename,'w') as f_obj:
json.dump(numbers, f_obj)
通常使用文件擴展名.json來指出文件存儲的數(shù)據(jù)為JSON格式。
函數(shù)json.load()可加載文件中的信息,并可存儲在一個變量中。
import json
filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)
print(numbers)
# 輸出:
[2, 3, 5, 7, 9, 11]
可見,輸出的列表與創(chuàng)建時的列表時一樣的,這是一種再程序間共享數(shù)據(jù)的簡單方式。
(2)保存和讀取用戶生成的數(shù)據(jù)
對于用戶生成的數(shù)據(jù),使用json保存它們大有裨益,因為如果不以某種方式進行存儲,等程序停止運行時用戶的信息將丟失。
接收用戶名并保存:
import json
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 + "!")
# 輸出:
What is your name? Eric
We'll remember you when you come back, Eric!
讀取信息:
import json
filename = 'username.json'
with open(filename) as f_obj:
username = json.load(f_obj)
print("Welcome back, " + username + "!")
# 輸出:
Welcome back, Eric!
現(xiàn)在將兩個程序合并到一個程序中。使得這個程序運行時,我們將嘗試從文件中獲取用戶名,因此需要首先編寫一個嘗試恢復(fù)用戶名的try代碼塊。如果這個文件不存在,我們就在except代碼塊中提示用戶輸入用戶名,并將其存儲在文件中,以便程序再次運行時能夠獲取它:
import json
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 + "!")
無論執(zhí)行的是except代碼塊還是else代碼塊,都將顯示用戶名和合適的問候語。如果這個程序是首次運行,將輸出:
What is your name? Eric
We'll remember you when you come back, Eric!
否則,輸出將如下:
Welcome back, Eric!
這是程序至少運行了一次時的輸出。
(3)重構(gòu)
代碼能夠正確地運行,但可做進一步的改進——將代碼劃分為一系列完成具體工作的函數(shù)。這樣的過程被稱為重構(gòu)。重構(gòu)讓代碼更清晰、更易于理解、更容易擴展。
要重構(gòu)上述代碼,可將其大部分邏輯放到一個或多個函數(shù)中。
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()
下面對該函數(shù)進行重構(gòu),讓它不執(zhí)行這么多任務(wù)。先將獲取存儲的用戶名的代碼移到另一個函數(shù)中:
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:
filename = 'username.json'
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 + "!")
greet_user()
新增的函數(shù)目標明確,如果存儲了用戶名,這個函數(shù)就獲取并返回它;如果文件不存在,就返回None。這種做法很好:函數(shù)要么返回預(yù)期的值,要么返回None;這讓我們能夠使用函數(shù)的返回值做簡單的測試。
我們還可以將greet_user()中的另一個代碼塊提取出來。將沒有存儲用戶名時提示用戶輸入的代碼放在一個獨立的函數(shù)中:
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 get_new_username():
filename = 'username.json'
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
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()
在這最終的版本中,每個函數(shù)都執(zhí)行單一而清晰的任務(wù)。我們調(diào)用greet_user(),它打印一條合適的消息:要么歡迎老用戶回來,要么問候新用戶。為此,它首先調(diào)用get_stored_username(),這個函數(shù)只負責獲取存儲的用戶名,再在必要時調(diào)用get_new_username(),這個函數(shù)只負責獲取并存儲新用戶的用戶名。要編寫出清晰而易于維護和擴展的代碼,這種劃分工作必不可少。