至此,我們掌握了編寫組織有序而易於使用的程序所需的基本技能,該考慮讓程序目標更明確,用途更大了。在本章中,我們將學習處理文件,讓程序能夠快速地分析大量的數據;我們將學習錯誤處理,避免程序在面對意外情形時崩潰;我們將學習異常,它們是Python創建的特殊對象,用於管理程序運行時出現的錯誤;我們還將學習模塊json,它讓我們能夠保存用戶數據,以免在程序停止運行后丟失。
學習處理文件和保存數據可以讓我們的程序使用起來更容易:用戶將能夠選擇輸入什么樣的數據,以及在什么時候輸入;用戶使用我們的程序做一些工作后,可將程序關閉,以后在接着往下做。學習處理異常可幫助我們應對文件不存在的情形,以及處理其他可能導致程序崩潰的問題。這讓我們的程序在面對錯誤的數據時更健壯——不管這些錯誤數據源自無意的錯誤,還是源自破壞程度的惡意企圖。我們在本章學習的技能可提高程序的適用性、可用性和穩定性。
10.1 從文件中讀取數據
文本文件可存儲的數據量多的難以置信:天氣數據、交通數據、社會經濟數據、文學作品等。每當需要分析或修改存儲在文件中的信息時,讀取文件都很有用,對數據分析應用程序來說尤其如此。例如,我們可以編寫一個這樣的程序:讀取一個文本文件的內容,重新設置這些數據的格式並將其寫入文件,讓瀏覽器能夠顯示這些內容。
要使用文本文件中的信息,首先需要將信息讀取到內存中。為此,我們可以一次性讀取文件的全部內容,也可以以每次一行的方式逐步讀取。
10.1.1 讀取整個文件
要讀取文件,需要一個包含幾行文本的文件。下面首先來創建一個文件,它包含精確到小數點后30位的圓周率值,且在小數點后每10處換行:
pi_digits.txt
3.1415926535
8979323846
2643383279
下面的程序打開並讀取這個文件,再將其內容顯示到屏幕上:
file_reader.py
with open('pi_digits') as file_object:
contents = file_object.read()
print(contents)
在這個程序中,第1行代碼做了大量的工作。我們先來看看函數open()。要以任何方式使用文件——哪怕僅僅是打印其內容,都得先打開文件,這樣才能
訪問它。函數open()接受一個參數:要打開的文件的名稱。Python在當前執行的文件所在的目錄中查找指定的文件。在這個示例中,當前運行的是file_reader.py,因此Python在file_reader.py所在的目錄中查找pi_digits.txt。函數open()返回一個表示文件的對象.在這里,open('pi_digits.txt')
返回一個表示文件pi_digits.txt的對象;Python將這個對象存儲在我們將在后面使用的變量中。
關鍵字with在不再需要訪問文件后將其關閉。在這個程序中,注意我們調用了open(),但沒有調用close();我們也可以調用open()和close()來打開和關閉文件,但這樣做時,如果程序存在bug,導致close()語句未執行,文件將不會關閉。這看似微不足道,但未妥善地關閉文件可能會導致數據丟失或受損。如果在程序中過早地調用close(),我們會發現需要使用文件時它已關閉(無法訪問),這會導致更多的錯誤。並非在任何情況下都能輕松確定關閉文件的恰當時機,但通過使用前面所示的結構,可讓Python去確定:我們只管打開文件,並在需要時使用它,Python自會在合適的時候自動將其關閉。
有了表示pi_digits的文件對象后,我們使用方法read()讀取這個文件的全部內容,並將其作為一個長長的字符串存儲在變量contents中。這樣,通過打印
contents的值,就可將這個文本文件的全部內容顯示出來。
3.1415926535
8979323846
2643383279
為何會多出這個空行呢?因為read()到達文件末尾時返回一個空字符串,而將這個空字符串顯示出來時就是一個空行。要刪除末尾的空行,可在print語句中使用rstrip():
with open('pi.digits') as file_object:
contents = file_object.read()
print(contents.rstrip())
本書前面說過,Python方法rstrip()刪除(剝除)字符串末尾的空白。現在,輸出與原始文件的內容完全相同:
3.1415926535
8979323846
2643383279
10.1.2 文件路徑
當我們將類似pi_digits.txt這樣的簡單文件名傳遞給open()時,Python將在當前執行的文件(即.py程序文件)所在的目錄中查找文件。
根據我們組織文件的方式,有時可能要打開不在程序文件所屬目錄中的文件.例如,我們可能將程序文件存儲在了文件夾python_work中,而在Python_work中,有一個名為text_files的文件夾,用於存儲文件操作的文本文件。雖然文件夾text_files包含在文件Python_work中,但僅向open()傳遞位於該文件夾中的文件的名稱也不可行,因為Python只在文件夾Python_work中查找,而不會在其子文件夾text_files中查找。要讓Python打開不與程序文件在同一個目錄中
的文件,需要提供文件路徑,它讓Python到系統的特定位置去查找。
由於文件夾text_files位於文件夾python_work中,因此可使用相對文件路徑來打開該文件夾中的文件。相對文件路徑讓Python到指定的位置去查找,而該位置是相對於當前運行程序所在目錄的。在Linux和OS X中,我們可以這樣編寫代碼:
with open('text_files/filename.txt') as file_object:
我們還可以將文件在計算機中的准確位置告訴Python,這樣就不用關心當前運行的程序存儲在什么地方了。這稱為絕對文件路徑。在相對文件行不通時,可使用絕對路徑。例如,如果text_files並不在文件夾Python_work中,而在文件夾other_files中,則向open()傳遞路徑'text_files/filename.txt'行不通,因為Python只在文件夾Python_work中查找該位置。為明確地指出我們希望Python到哪里去查找,我們需要提供完整的路徑。
絕對路徑通常比相對路徑更長,因此將其存儲在一個變量中,因此將其存儲在一個變量中,再將該變量傳遞給open()會有所幫助。在Linux和OS X中,絕對路徑類似於下面這樣:
file_path = '/home/ehmattes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
通過使用絕對路徑,可讀取系統任何地方的文件。就目前而言,最簡單的做法是,要么將數據文件存儲在程序文件所在的目錄,要么將其存儲在程序文件所在目錄下的一個文件夾(如text_files)中。
注意: Windows系統有時候能夠正確地解讀文件路徑中的斜杠。如果你使用的是Windows系統,且結果不符合預期,請確保在文件路徑中使用的是反斜杠。另外,由於反斜杠在Python中被視為轉義標記,為在Windows中確保萬無一失,應以原始字符串的方式指定路徑,即在開頭的單引號前加上r.
絕對路徑:
file_path = '/home/zhuzhu/title10/pi_digits'
with open(file_path) as file_object:
cotents = file_object.read()
print(cotents.rstrip())
10.1.3 逐行讀取
讀取文件時,常常需要檢查其中的每一行:我們可能要在文件中查找特定的信息,或者要以某種方式修改文件中的文本。例如,我們可能要遍歷一個包含天氣數據的文件,並使用天氣描述中包含字樣sunny的行。在新聞報道中,我們可能會查找包含標簽<headline>的行,並按特定的格式設置它。
要以每次一行的方式檢查文件,可對文件對象使用for循環:
file_reader.py
filename = 'pi_digits' --(1)
with open(filename) as file_object: --(2)
for line in file_object: --(3)
print(line)
在(1)處,我們將要讀取的文件的名稱存儲在變量filename中,這是使用文件時一種常見的做法。由於變量filename表示的並非實際文件——它只是一個讓Python知道到哪里去查找文件的字符串,因此可輕松地將'pi_digits'替換為你要使用的另一個文件的名稱。調用open()后,將一個表示文件及其內容的對象存儲到了變量file_object中。這里也使用了關鍵字with,讓Python負責妥善地打開和關閉文件。為查看文件的內容,我們通過對文件對象執行循環來遍歷文件中的每一行。
我們打印每一行,發現空白行更多了:
3.1415926535
8979323846
2643383279
為何會出現這些空白行呢?因為在這個文件中,每行的末尾都有一個看不見的換行符,而print語句也會加上一個換行符,因此每行末尾都
有兩個換行符:一個來自文件,另一個來自print語句。要消除這些換行符,可在print語句中使用方法rstrip():
with open('pi_digits') as file_object:
for line in file_object:
print(line.rstrip())
現在,輸出由於文件內容完全相同了:
3.1415926535
8979323846
2643383279
10.1.4 創建一個包含文件各行內容的列表
使用關鍵字with時,open()返回的文件對象只在with代碼塊內可用。如果要在with代碼塊訪問文件的內容,可在with代碼塊內將文件的各行存儲在一個列表中,並在with代碼塊外使用該列表:我們可用立即處理文件的各個部分,也可推遲到程序后面再處理。
下面的示例在with代碼塊中將文件pi_digits的各行存儲在一個列表中,再在with代碼塊外打印它們:
filename = 'pi_digits'
with open(filename) as file_object:
lines = file_object.readlines() --(1)
print(lines)
for line in lines: --(2)
print(line.rstrip())
(1)處的方法readlines()從文件中讀取每一行,並將其存儲在一個列表中;接下來,該列表被存儲到變量lines中;在with代碼塊外,我們依然可以使用這個變量。在(2)處,我們使用一個簡單的for循環來打印lines中的各行。由於列表lines的每個元素都對應於文件中的一行,因此輸出與文件內容完全一致:
['3.1415926535\n', ' 8979323846\n', ' 2643383279\n']
3.1415926535
8979323846
2643383279
可以看出,當我們輸出列表的時候,在每個元素的末尾都有一個'\n',說明存在一個換行符,我們看不見,但是會存在,因此使用這個列表的時候也應該注意。
10.1.5 使用文件的內容
將文件讀取到內存中后,就可以以任何方式使用這些數據了。下面以簡單的方式使用圓周率的值。首先,我們創建一個字符串,它包含文件中存儲的所有數字,且沒有任何空格:
filename = 'pi_digits'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = "" --(1)
for line in lines: --(2)
pi_string += line.rstrip()
print(pi_string) --(3)
print(len(pi_string))
就像前一個實例一樣,我們首先打開文件,並將其中的所有行都存儲在一個列表中。在(1)處,我們創建了一個變量pi_string,用於存儲圓周率。
接下來,我們使用一個循環將各行都加入pi_string,並刪除每行末尾的換行符。在(3)處,我們打印這個字符串及其長度:
3.1415926535 8979323846 2643383279
36
在變量pi_string存儲的字符串中,包含原來位於每行左邊的空格,為刪除這些空格,可使用strip()而不是rstrip():
filename = 'pi_digits'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ""
for line in lines:
pi_string += line.strip()
print(pi_string)
print(len(pi_string))
這樣,我們就獲得了一個這樣的字符串:它包含精確到30位小數的圓周率值。這個字符串長32字符,因為它還包含整數部分的3和小數點:
3.141592653589793238462643383279
32
注意:讀取文本文件時,Python將其中的所有文本都解讀為字符串。如果我們讀取的是數字,並要將其作為數值使用,就必須使用函數int()將其
轉換為整數,或使用函數float()將其轉換為浮點數。
10.1.6 包含一百萬的大型文件
前面我們分析的都是一個只有三行的文本文件,但這些代碼示例也可以處理大得多的文件。如果我們有一個文本文件,其中包含精確到小數點后1000000位而不是30位的圓周率值,也可創建一個包含這些數字的字符串。為此,我們無需對前面的程序做任何修改,只需將這個文件傳遞給它即可。
在這里,我們只打印到小數點后50位,以免終端會顯示全部1000000位而不斷地翻滾:
filename = 'pi_digits'
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))
輸出表明,我們創建的字符串確實包含精確到小數點后1000000位的圓周率值:
3.14159265358979323846264338327944444444444444444444......
658
對於我們可處理的數據量,Python沒有任何限制;只要系統的內存足夠多,我們想處理多少數據都可以。
10.1.7 圓周率值中包含你的生日嗎
我一想知道自己的生日是否包含在圓周率中。下面來擴展剛才編寫的程序,以確定某個人的生日是否包含在圓周率值的前1000000位中。為此,可將生日表示為一個由數字組成的字符串,再檢查這個字符串是否包含在pi_string中:
filename = 'pi_digits'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ""
for line in lines:
pi_string += line.strip()
birthday = input("Enter your birthday,in the form mmddyy: ") --(1)
if birthday in pi_string: --(2)
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appears in the first million digits of pi!")
在(1)處,我們提示用戶輸入其生日,在接下來的(2)處,我們檢查這個字符串是否包含在pi_string中。運行一下這個程序:
Enter your birthday,in the form mmddyy: 122689
Your birthday appears in the first million digits of pi!
我的生日確實出現在了圓周率值中!讀取文件的內容后,就可以以我們能想到的任何方式對其進行分析。
動手試一試
10-1 Python學習筆記:在文本編輯器中新建一個文件,寫幾句話來總結一下你至此學到的Python只是,其中每一行都以“In Python you can”打頭。將這個文件命名為learning_python.txt,並將其存儲到為完成本章練習而編寫的程序所在的目錄中。編寫一個程序,它讀取這個文件,並將我們所寫的內容打印三次:第一次打印是讀取整個文件;第二次打印時遍歷文件對象;第三次及打印是將各行存儲在一個列表中,再在with代碼塊外打印它們。
filename = 'lenrning_python'
with open(filename) as file_object:
contents = file_object.read()
print(contents)
with open(filename) as file_object:
for line in file_object:
print(line.strip())
print("\n")
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.strip())
運行結果如下:
In Python you can lenrn how to develop game.
In python you can learn how to do peration and maintenance.
In python you can lenrn how to do data analysis.
In python you can learn a lot of things which you can think.
In Python you can lenrn how to develop game.
In python you can learn how to do peration and maintenance.
In python you can lenrn how to do data analysis.
In python you can learn a lot of things which you can think.
In Python you can lenrn how to develop game.
In python you can learn how to do peration and maintenance.
In python you can lenrn how to do data analysis.
In python you can learn a lot of things which you can think.
10-2 C語言學習筆記:可使用方法replace()將字符串中的特定單詞都替換為另一個單詞。下面是一個簡單的示例,演示了如何將句子中的'dog'替換為'cat':
>>> message = "I really like dogs"
>>> message.replace('dog','cat')
'I really like cats'
讀取我們剛創建的文件lenrning_python.txt中的每一行,將其中的Python都替換為另一門語言的名稱,如C。將修改后的各行都打印到屏幕上。
filename = 'lenrning_python'
lines = []
with open(filename) as file_object:
for line in file_object:
string = line.replace('python',"C")
lines.append(string)
for message in lines:
print(message.strip())
運行如果如下:
In C you can lenrn how to develop game.
In C you can learn how to do peration and maintenance.
In C you can lenrn how to do data analysis.
In C you can learn a lot of things which you can think.
10.2 寫入文件
保存數據的最簡單方式之一是將其寫入到文件中。通過將輸出寫入文件,即便關閉包含程序輸出的終端窗口,這些輸出也依然存在:我們可以在程序結束
運行后查看這些輸出,可與別人分享輸出文件,還可以編寫程序來將這些輸出讀取到內存中並進行處理。
10.2.1 寫入空文件
要將文本寫入文件,我們在調用open()時需要提供另一個實參,告訴Python我們要寫入打開的文件。為明白其中的工作原理,我們來看一條簡單的消息存
儲到文件中,而不是將其打印到屏幕上:
write_message.py
filename = 'programming.txt'
with open(filename,'w') as file_object: --(1)
file_object.write("I love programming.") --(2)
在這個示例中,調用open()時提供了兩個實參(見1).第一個實參也是要打開的文件的名稱;第二個實參('w')告訴Python,我們要以寫入模式打開這個文件
。打開文件時,可指定讀取模式('r')、寫入模式('w')、附加模式('a')、或者讓我們能夠讀取和寫人文件的模式讀寫模式('r+').如果我們省略了模式實參,
Python將以默認的只讀模式打開文件。
如果我們要寫人的文件不存在,函數open()將自動創建它。然而,以寫入('w')模式打開文件時千萬要小心,因為如果指定的文件已經存在,Python將在
返回文件對象前清空該文件。
在(2)處,我們使用文件對象的方法write()將一個字符串寫入文件。這個程序沒有終端輸出,但如果我們打開文件programming.txt,將看到其中包含如
下一行內容:
programming.txt
I love programming.
相比於我們的計算機中的其他文件,這個文件沒有什么不同。我們可以打開它、在其中輸入新文本、復制其內容,將內容粘貼到其中等。
注意:Python只能將字符串寫入文本文件。要將數值數據存儲到文本文件中,必須先使用函數str()將其轉換為字符串格式。
10.2.2 寫入多行
函數write()不會在你寫入的文本末尾添加換行符,因此如果我們寫入多行時沒有指定換行符,文本看起來可能不是我們希望的那樣:
write_message.py
filename = 'programming.txt'
with open(filename,'w') as file_object:
file_object.write("I love programming.")
file_object.write("I love creating new games.")
我們打開programming.txt,將發現兩行內容擠在一起:
I love programming.I love creating new games.
要讓每個字符串都單獨占一行,需要在write()語句中包含換行符:
filename = 'programing.txt'
with open(filename,'w') as file_object:
file_object.write("I love programming.\n")
file_object.write("I love creating new games.\n")
現在,輸出出現在不同行中:
I love programming.
I love creating new games.
像顯示到終端的輸出一樣,還可以使用空格、制表符和空行來設置這些輸出的格式。
10.2.3 附加到文件
如果我們要給文件添加內容,而不是覆蓋原有的內容,可以附加模式打開文件。我們以附加模式打開文件時,Python不會在返回文件對象前清空文件,而
寫入到文件的行都將添加到文件末尾。如果指定的文件不存在,Python將為我們創建一個空文件。
下面來修改write_message.py,在既有文件programming.txt中再添加一些我們酷愛編程的原因:
filename = 'programming.txt'
with open(filename,'a') as file_object: --(1)
file_object.write("I also love finding meaning in large datasets.\n") --(2)
file_object.write("I love creating apps that can run in a browser.\n")
在(1)處,我們打開文件時指定了實參'a',以便將內容附加到文件末尾,而不是覆蓋文件原來的內容。在(2)處,我們有寫入了兩行,它們被添加到文件
programming.txt末尾:
programming.txt
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps tan can run in a browser.
最終的結果是,文件原來的內容還在,它們后面是我們剛添加的內容。
動手試一試
10-3 訪客:編寫一個程序,提示用戶輸入其名字;用戶做出響應后,將其名字寫入到文件guest.txt中。
分析:由於是寫入,因為我們不可能覆蓋前面一個來訪的人,這也是不被允許的,因為采用的追加模式。
filename = 'guest.txt'
with open(filename,'a') as file_object:
guest = input("Please input your name: ")
file_object.write(guest.title() + "\n")
10-4 訪客名單:編寫一個while循環,提示用戶輸入其名字,用戶輸入名字后,在屏幕上打印一條問候語,並將一條訪問記錄添加到文件
guest_book.txt中。確保這個文件中的每天記錄都獨占一行。
filename = 'guest_book.txt'
with open(filename,'a') as file_object:
while True:
print("Enter 'q' to quit the programmer: ")
guest = input("Please input your name: ")
if guest == 'q':
break
else:
print("Hello, " + guest)
file_object.write(guest.title() + "\n")
運行結果如下:
Enter 'q' to quit the programmer:
Please input your name: geng changxue
Hello, geng changxue
Enter 'q' to quit the programmer:
Please input your name: zeng mingzhu
Hello, zeng mingzhu
Enter 'q' to quit the programmer:
Please input your name: zhang dabiao
Hello, zhang dabiao
Enter 'q' to quit the programmer:
Please input your name: q
10.3 異常
Python使用被稱為異常的特殊對象來管理程序執行期間發生的錯誤。每當發生讓Python不知所措的錯誤時,它都會創建一個異常對象。如果我們編寫了處
理該異常的代碼,程序將繼續運行;如果我們未對異常進行處理,程序將停止,並顯示traceback,其中包含有關異常的報告。
異常是使用try-except代碼塊處理的。try-except代碼塊讓Python執行指定的操作,同時報告Python發生了異常時怎么辦。使用了try-except代碼塊時
,即便出現異常,程序也將繼續運行:顯示我們編寫的友好的錯誤消息,而不是令用戶迷惑的traceback。
10.3.1 處理ZeroDivisionError異常
下面來看一種導致Python引發異常的簡單錯誤。我們知道不能將一個數字除以0,但我們讓Python這樣做吧:
division.py
print(5/0)
顯然,Python無法這樣做,因此我們將看到一個traceback:
Traceback (most recent call last):
File "/home/zhuzhu/title10/division.py", line 1, in <module>
print(5/0)
ZeroDivisionError: division by zero --(1)
在上述traceback中,(1)處指出的錯誤ZeroDivisionError是一個異常對象。Python無法按照我們的要求做時,就會創建這種對象。在這種情況下,
Python將停止運行程序,並指出引發了哪種異常,而我們可根據這些信息對程序進行修改。下面我們告訴Python,發生這種錯誤時怎么辦;這樣,如果再次
發生這樣的錯誤,我們就有備無患了。
10.3.2 使用try-except代碼塊
當我們認為可能發生了錯誤時,可編寫一個try-except代碼塊來處理可能引發的異常。我們讓Python嘗試運行一些代碼,並告訴它如果這些代碼引發了
指定的異常,該怎么辦。
處理ZeroDivisionError異常的try-except類似於下面這樣:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
我們將導致錯我的代碼行print(5/0)放在了一個try代碼塊中。如果try代碼塊中的代碼運行起來沒有問題,Python將跳過except代碼塊;如果try代碼
塊中的代碼導致了錯誤,Python將查找這樣的except代碼塊,並運行其中的代碼,即其中指定的錯誤與引發錯誤的相同。
在這個實例中,try代碼塊中的代碼引發了ZeroDivisionError異常,因此Python指出了該如何解決問題的except代碼塊,並運行其中的代碼。這樣,用戶
看到的是一條友好的錯誤消息,而不是traceback:
You can't divide by zero!
如果try-except代碼塊后面還有其他代碼,程序將接着運行,因為已經告訴了Python如何處理這種錯誤。下面來看一個捕獲錯誤后程序將繼續運行的示例
10.3.3 使用異常避免崩潰
發生錯誤時,如果程序還有工作沒有完成,妥善地處理錯誤尤其重要。這種情況經常會出現在要求用戶提供輸入的程序中;如果程序能夠妥善地處理無效
輸入,就能再提示用戶提供有效輸入,而不至於崩潰。
下面來創建一個只執行除法運算的計算器:
division.py
print("Give me two numbers,and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFrist number: ") --(1)
if first_number == 'q':
break
second_number = input("\nSecond number: ") --(2)
if second_number == 'q':
break
answer = int(first_number)/int(second_number) --(3)
print(answer)
在(1)處,這個程序提示用戶輸入一個數字,並將其存儲到變量first_number中;如果用戶輸入的不是退出的'q',就再提示用戶輸入一個數字,並將其
存儲到變量second_number中;接下來,我們計算這兩個數字的商(即answer).這個程序沒有采取任何處理錯誤的措施,因此讓它執行除數為0的除法運算時,
它將崩潰:
Give me two numers,and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "/home/zhuzhu/title10/divide.py", line 11, in <module>
print(float(divisor)/float(dividend))
ZeroDivisionError: float division by zero
程序崩潰可不好,但讓用戶看到traceback也不是好主意。不懂技術的用戶會被它們搞糊塗,而且如果用戶懷有惡意,他會通過traceback獲悉我們不希望
他知道的信息。例如,他將知道你的程序文件的名稱,還將看到部分不能正常運行的代碼。有時候,訓練有素的攻擊者可根據這些信息判斷出可對我們的代碼
發起什么樣的攻擊。
10.3.4 else代碼塊
通過將可能引發錯誤的代碼塊放在try-except代碼塊中,可提高這個程序抵御錯誤的能力。錯誤是執行除法運算的代碼行導致的,因此我們需要將它們放
到try-except代碼塊中。這個示例還包含一個else代碼塊;依賴於try代碼塊成功執行的代碼都應放到else代碼塊中:
print("Give me two numbers,and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFrist number: ") --(1)
if first_number == 'q':
break
second_number = input("\nSecond number: ") --(2)
if second_number == 'q':
break
try:
answer = int(first_number)/int(second_number) --(3)
except ZeroDivisionError:
print("You can't divide by zero!")
else:
print(answer)
我們讓Python嘗試執行try代碼塊中的除法運算(見1),這個代碼塊只包含可能導致錯誤的代碼。依賴於成功執行的代碼塊都放在else代碼塊中;在這個
示例中,如果除法運算成功,我們就使用else代碼塊來打印結果。
except代碼塊告訴Python,出現ZeroDivisionError異常時該怎么辦。如果try代碼塊因除零錯誤而失敗,我們就打印一條友好的消息,告訴用戶如何
避免這種錯誤。程序將繼續運行,用戶根本看不到traceback:
Give me two numbers,and I'll divide them.
Enter 'q' to quit.
Frist number: 5
Second number: 0
Yor can't divide by zero!
Frist number: 5
Second number: 2
2.5
Frist number: q
try-except-else代碼塊的工作原理大致如下:Python嘗試執行try代碼塊中的代碼;只有可能引發異常的代碼塊才需要放在try語句中。有時候,有
一些僅在try代碼塊成功執行時才需要運行的代碼;這些代碼應放在else代碼塊中。except代碼塊告訴Python,如果它嘗試運行try代碼塊中的代碼時引發了
指定的異常,該怎么辦。
通過預測可能發生錯誤的代碼,可編寫健壯的程序,它們即便面臨無效數據或缺少資源,也能繼續運行,從而能抵御無意的用戶錯誤和惡意的攻擊。
10.3.5 處理FileNotFoundError異常
使用文件時,一種常見的問題是找不到文件:我們要查找的文件可能在其他地方、文件名可能不正確或者這個文件根本就不存在。對於所有這些情形,
都可使用try-except代碼塊以直觀的方式進行處理。
我們來嘗試讀取一個不存在的文件。下面的程序嘗試讀取文件alice.txt的內容,但我們沒有將這個文件名存儲在alice.py所在的目錄中:
alice.py
filename = 'alice.txt'
with open(filename) as f_obj:
contents = f_obj.read()
Python無法讀取不存在的文件,因此它引發一個異常:
Traceback (most recent call last):
File "/home/zhuzhu/title10/alice.py", line 3, in <module>
with open(filename) as f_obj:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
在上述traceback中,最后一行報告了FileNotFoundError異常,這是Python找不到要打開的文件時創建的異常。在這個示例中,這個錯誤是函數open()
導致的,因此要處理這個錯誤,必須將try語句放在包含open()的代碼行之前:
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)
在這個示例中,try代碼塊引發FileNotFoundError異常,因此Python找出與該錯誤匹配的except代碼塊,並運行其中的代碼。最終的結果是顯示一條友
好的錯誤消息,而不是traceback:
Sorry, the file alice.txt does not exist.
如果文件不存在,這個程序什么都不做,因此錯誤處理代碼的意義不打。下面來擴展這個示例。看看在我們使用這個文件時,異常處理可提供什么樣的幫
助。
10.3.6 分析文本
我們可以分析包含整本書的文本文件。很多經典文學作品都是以簡單的文本文件的方式提供的,因為它們不受版權限制。
下面來提取童話Alice in Wonderland的文本,並嘗試計算它包含多少個單詞。我們將使用方法split(),它根據一個字符串創建一個單詞列表。下面是對
只包含童話名"Alice in Wonderland"的字符串調用方法split()的結果:
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']
方法split()以空格為分隔符將字符串分拆成多個部分,並將這些部分都存儲到一個列表中。結果是一個包含字符串中所有單詞的列表,雖然有些單詞可
能包含標點。為計算機Alice in Wonderland包含多少個單詞,我們將對整篇小說調用split(),在計算得到的列表中包含多少個元素,從而確定整篇童話大
致包含多少個單詞:
filename = 'pi_digits'
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() --(1)
num_words = len(words) --(2)
print("The file " + filename + " has about " + str(num_words) + ' words.') --(3)
我們把文件alice.txt移到了正確的目錄中,讓try代碼塊能夠成功地執行。在(1)處,我們對變量contents(它現在是一個長長的字符串,包含童話Alice
in Wonderland的全部文本)調用方法split(),以生成一個列表,其中包含這部童話中的所有單詞。當我們使用len()來確定這個列表的長度時,就知道了
原始字符串大致包含了多少個單詞(見(2))。在(3)處,我們打印一條消息,指出文件包含多少個單詞。這些代碼都放在else代碼塊中,因為僅當try代碼塊
成功執行時才執行它們。輸出指出了文件alice.txt包含多少個單詞:
The file pi_digits has about 11 words.
這個數字有點大,因為這里使用的文本文件包含出版商提供的額外信息,但與童話Alice in Wonderland的長度相當一致。
10.3.7 使用多個文件
下面多分析幾本書。這樣做之前,我們先將這個程序的大部分代碼移到一個名為count_words()的函數中,這樣對多本書進行分析時將更容易:
Word_count.py
def count_words(filename):
"""計算一個文件大致包含多少個單詞"""
'''判斷這個文件存在不存在,以免出現讀取錯誤'''
try:
with open(filename) as f_obj:
'''讀取文件,並將文件以字符串形式存放在一個變量中'''
contents = f_obj.read()
except FileNotFoundError:
print("The " + filename + " is not exist.")
else:
words = contents.split()
#把一系列字符串拆分成一個個單詞
num_words = len(words)
#統計單詞個數
print("The " + filename + " has about " + str(num_words) + " words.")
這些代碼大都與原來一樣,我們只是將它們移到了函數count_words()中,並增加了縮進量。修改程序的同時更新注釋是個不錯的習慣,因此我們將注釋
改成了文檔字符串,並稍微調整了一下措辭。
現在可以編寫一個簡單的循環,計算要分析的任何文本包含多少個單詞了。為此,我們將要分析的文件名稱存放在一個列表中,然后對這個列表中的每個
文件都調用count_words()。我們將嘗試Alice in Wonderland、Siddharha、Moby Dick和Little Women分別包含了多少個單詞。
filenames = ['pi_digits',"gcx",'guest.txt','guest_book.txt']
for filename in filenames:
count_words(filename)
文件gcx不存在,但這絲毫不影響這個程序處理其他文件:
The pi_digits has about 11 words.
The gcx is not exist.
The guest.txt has about 4 words.
The guest_book.txt has about 6 words.
在這個實例中,使用try-except-else代碼塊提供了兩個重要的優點:避免讓用戶看到traceback;讓程序能夠繼續分析能夠找到的其他文件。如果不捕
獲因找不到gcx而引發的FileNotFoundError異常,用戶將看到完整的traceback,而程序將在嘗試分析gcx后停止運行——根本不分析guest.txt和guest_book
10.3.8 失敗時一聲不吭
在前一個示例中,我們告訴用戶有一個文件找不到。但並非每次捕獲到異常時都需要告訴用戶,有時候我們希望程序在發生異常時一聲不吭,就像什么都
沒有發生一樣繼續運行。要讓程序在失敗時一聲不吭,可像通常那樣編寫try代碼塊,但在except代碼塊中明確地告訴Python什么都不要做。Python有一個
pass語句,可在代碼塊中使用它來讓Python什么都不要做:
def count_words(filename):
"""計算一個文件大致包含多少個單詞"""
'''判斷這個文件存在不存在,以免出現讀取錯誤'''
try:
with open(filename) as f_obj:
'''讀取文件,並將文件以字符串形式存放在一個變量中'''
contents = f_obj.read()
except FileNotFoundError:
pass --告訴Python什么都不做,直接跳過,執行下一個
else:
words = contents.split()
#把一系列字符串拆分成一個個單詞
num_words = len(words)
#統計單詞個數
print("The " + filename + " has about " + str(num_words) + " words.")
filenames = ['pi_digits',"gcx",'guest.txt','guest_book.txt']
for filename in filenames:
count_words(filename)
相比於前一個程序,這個程序唯一不同的地方是(1)處的pass語句。現在,出現FileNotFoundError異常時,將執行except代碼塊中的代碼,但什么都不
會發生。這種錯誤發生時,不會出現traceback,也沒有任何輸出。用戶將看到存在每個文件包含多少個單詞,但沒有任何跡象表明有一個文件未找到:
The pi_digits has about 11 words.
The guest.txt has about 4 words.
The guest_book.txt has about 6 words.
pass語句還充當了占位符,它提醒我們在程序的某個地方什么都沒有做,並且以后也許要在這里做些什么。例如,在這個程序中,我們可能決定將找不到
的文件的名稱寫入到文件missing_file.txt中。用戶看不到這個文件,但我們可以讀取這個文件,進而處理所有文件找不到的問題。
def count_words(filename):
"""計算一個文件大致包含多少個單詞"""
'''判斷這個文件存在不存在,以免出現讀取錯誤'''
try:
with open(filename) as f_obj:
'''讀取文件,並將文件以字符串形式存放在一個變量中'''
contents = f_obj.read()
except FileNotFoundEr --在except中加入語句,將找不到的文件存儲到一個專門的文件中,用於接下來的分析
with open('missing_files','a') as file_obj:
file_obj.write(filename + '\n')
else:
words = contents.split()
#把一系列字符串拆分成一個個單詞
num_words = len(words)
#統計單詞個數
print("The " + filename + " has about " + str(num_words) + " words.")
filenames = ['pi_digits',"gcx",'guest.txt','guest_book.txt']
for filename in filenames:
count_words(filename)
分析:在上面程序中,我們在except中加入了語句,我們希望如果這個文件不存在,將這個文件存儲起來,以便我們在用戶運行之后能夠進行分析。這種情況
也是很常見的問題,在我們以后用到的時候會很常見
10.3.9 決定報告哪些錯誤
在什么情況下該向用戶報告錯誤?在什么情況下又應該一聲不吭呢(pass)?如果用戶知道要分析哪些文件,它們可能希望在文件沒有分析時出現一條消息,
將其中的原因告訴它們。如果用戶只想看到結果,而並不知道要分析哪些文件,可能就無需在有些文件不存在時告訴它們。向用戶顯示他不想看到的信息可能
會降低程序的可用性。Python的錯誤處理結構讓我們能夠細致地控制與用戶分享錯誤信息的程度,要分享多少信息由我們決定。
編寫的很好且經過詳細測試的代碼不容易出現內部錯誤,如語法或邏輯錯誤,但只要程序依賴於外部因素,如用戶輸入、存在指定的文件、有網絡鏈接,
就有可能出現異常。憑借經驗可判斷該在程序的什么地方包含異常處理塊,以及出現錯誤時該向用戶提供多少相關的信息。
動手試一試
10-6 加法運算:提示用戶提供數值輸入時,常出現的一個問題是,用戶提供的是文本而不是數字。在這種情況下,當我們嘗試將輸入轉換為整數時,
將引發TypeError異常。編寫一個程序,提示用戶輸入兩個數字,再將它們相加並打印結果。在用戶輸入的任何一個值不是數字時都捕獲TypeError異常,並
打印一條友好的錯誤消息。對我們編寫的程序進行測試:先輸入兩個數字,再輸入一些文本而不是數字。
10.4 存儲數據
很多程序都要求用戶輸入某種信息,如讓用戶存儲游戲首選項或提供要可視化的數據。不管專注的是什么,程序都把用戶提供的信息存儲在列表和字典
等數據結構中。用戶關閉程序時,我們幾乎總是要保存他們提供的信息:一種簡單的方式是使用模塊json來存儲數據。
模塊json讓我們能夠將簡單的Python數據結構轉儲到文件中,並在程序再次運行時加載該文件中的數據。我們還可以使用json在Python程序之間分
享數據。更重要的是,JSON數據格式並非Python專用的,這讓我們能夠將以JSON格式存儲的數據與使用其他編程語言的人分享。這是一種輕便格式,很有用,
也易於學習。
10.4.1 使用json.dump()和json.load()
我們來編寫一個存儲一組數字的簡短程序,在編寫一個將這些數字讀取到內存中的程序。第一個程序將使用json.dump()來存儲這組數據,而第二個程序
將使用json.load()。
函數json.dump()接受兩個實參:要存儲的數據以及可用於存儲數據的文件對象。下面演示了如何使用json.dump()來存儲數字列表:
number_writer.py
import json
numbers = [2,3,5,7,11,13]
filename = 'numbers.json' (1)
with open(filename,'w') as f_obj: (2)
json.dump(numbers,f_obj) (3)
我們先導入模塊json,再創建一個數字列表。在(1)處,我們指定了要將該數字列表存儲到其中的文件的名稱。通常使用文件擴展名json來指出文件存儲的
數據為JSON格式。接下來,我們以寫入模式打開這個文件,讓JSON能夠將數據寫入其中。在(3)處,我們使用函數json.dump()將數字列表存儲到文件numbers.
json中。
這個程序沒有輸出,但我們可以打開文件numbers.json,看看其內容。數據的存儲格式與Python中一樣:
[2, 3, 5, 7, 11, 13]
下面再編寫一個程序,使用json.load()將這個列表讀取到內存中:
number_reader.py
import json
filename = 'numbers.json' (1)
with open(filename) as f_obj: (2)
numbers = json.load(f_obj) (3)
print(numbers)
在(1)處,我們確保讀取的是前面寫入的文件。這次我們以讀取方式打開這個文件,因為Python只需讀取這個文件(見(2))。在(3)處,我們使用函數json.
load()加載存儲在numbers.json中的信息,並將其存儲到變量numbers中。最后,我們打印恢復的數字列表,看看它是否與number_write.py中創建的數字列表
相同。
[2, 3, 5, 7, 11, 13]
這是一種在程序之間共享數據的簡單方式。
10.4.2 保存和讀取用戶生成的數據
對於用戶生成的數據,使用json保存它們大有裨益,因為如果不以某種方式進行存儲,等程序停止運行時用戶的信息將丟失。下面來看一個這樣的例子:
用戶首次運行程序時被提示輸入自己的名字,這樣再次運行程序時就記住他了。
我們先來存儲用戶的名字:
remember_me.py
import json
username = input("What is your name? ") (1)
filename = 'username.json'
with open(filename,'w') as f_obj:
json.dump(username,f_obj) (2)
print("We'll remember you when you come back," + username + "!") (3)
在(1)處,我們提示輸入用戶名,並將其存儲在一個變量中。接下來,我們調用json.dump(),並將用戶名和一個文件對象傳遞給它,從而將用戶名存儲到
文件中。然后,我們打印一條消息,指出我們存儲了他輸入的信息:
What is your name? soria aoi
We'll remember you when you come back, Soria Aoi!
現在在編寫一個程序,向其名字被存儲的用戶發出問候:
greet_user.py
import json
filename = 'username.json'
with open(filename) as f_obj:
username = json.load(f_obj) (1)
print("welcome back, " + username + "!") (2)
在(1)處,我們使用json.load()將存儲在username.json中的信息讀取到變量username中。恢復用戶名后,我們就可以歡迎用戶回來了。
Welcome back, soria aoi!
我們需要將這兩個程序合並到一個程序(remember_me.py)中。這個程序運行時,我們嘗試從文件username.json中獲取用戶名,因此我們首先編寫一個嘗
試恢復用戶名的try代碼塊。如果這個文件不存在,我們就在except代碼塊中提示用戶輸入用戶名,並將其存儲在username.json中,以便程序再次運行時能
夠獲取它:
remember_me.py
import json
#如果以前存儲了用戶名,就加載它
#否則,就提示用戶輸入用戶名並存儲它
filename = 'username.json'
try:
#判斷文件是否存在
with open(filename) as f_obj: (1)
username = json.load(f_obj) (2)
except FileNotFoundError: (3)
#文件找不到,創建一個文件,存儲用戶名
username = input("What is your name? ") (4)
with open(filename,"w") as f_obj: (5)
json.dump(username,f_obj)
print("We'll remember you when you come back, "+ username + "!")
else:
print("Welcome back, " + username +"!")
這里沒有任何新代碼,只是將前兩個示例的代碼合並到了一個程序中。在(1)處,我們嘗試打開文件username.json。如果這個文件存在,就將其中的用戶
名讀取到內存中(見(2)),再執行else代碼塊,即打印一條歡迎用戶回來的消息。用戶首次運行這個程序時,文件username.json不存在,將引發FileNot
FondError異常(見(3)),因此Python將執行except代碼塊:提示用戶輸入其用戶名(見(4)),再使用json.dump()存儲該用戶名,並打印一句問候語。
無論執行的是except代碼塊還是else代碼塊,都將顯示用戶名和合適的問候語。
10.4.3 重構
我們經常會遇到這樣的情況:代碼能夠正確地運行,但可做進一步的改進——將代碼划分為一系列完成具體工作的函數。這樣的過程被稱為重構。重構讓
代碼更清晰、更易於理解、更容易擴展。
要重構remember_me.py,可將其大部分邏輯放到一個或多個函數中。remember_me.py的重點是問候用戶,因此我們將其所有代碼都放到一個名為greet_
user()的函數中:
remember_me.py
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,filename)
print("We'll remember your 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:
filename = 'username.json'
username = input("What is your name? ")
with open(filename,'w') as f_obj:
json.dump(username,filename)
print("We'll remember you when you come bace, " + username + "!")
greet_user()
新增的函數get_stored_username()目標明確,(1)處的文檔字符串指出了這一點.如果存儲了用戶名,這個函數就獲取並返回它,如果文件username.json
不存在,這個函數就返回None。這是一種不錯的做法:函數要么返回預期的值,要么返回None;這讓我們能夠使用函數的返回值做簡單測試。在(3)處,如果成
功地獲取了用戶名,就打印一條歡迎回來的消息,否則就提示用戶輸入用戶名。
我們還需將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 get_new_username():
'''提示用戶輸入用戶名'''
username = input("What is your name? ")
filename = 'username.json'
with open(filename,'w') as f_obj:
json.dump(username,filename)
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 caome back, " + username + "!")
greet_user()
在remember_me.py這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user(),它打印一條合適的消息:要么歡迎老用戶回來,要么問
候新用戶。為此,它首先調用get_stored_username(),這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username(),這個函
數只復雜獲取並存儲新的用戶的用戶名。要編寫出清晰而易於維護和擴展的代碼,這種划分工作必不可少。
動手試一試
10-11 喜歡的數字:編寫一個程序,提示用戶輸入他喜歡的數字,並使用json.dump()將這個數字存儲到文件中。在編寫一個程序,從文件中讀取這個
值並打印消息"I know your favorite number!It's ."
favorite_num.py
import json
filename = 'favorite.json'
with open(filename,'w') as f_obj:
favorite_num = input("Please input your favorite number: ")
json.dump(favorite_num,f_obj)
print("We,'ll remember your favorite number.")
favorite_read.py
import json
filename = 'favorite.json'
with open(filename) as f_obj:
favorite_num = json.load(f_obj)
print("I know your favorite number! It's " + str(favorite_num) + ".")
10-12 記住喜歡的數字:將練習10-11中的兩個程序合二為一。如果存儲了用戶喜歡的數字,就向用戶顯示它,否則提示用戶輸入他喜歡的數字並將其
存儲到文件中。運行這個程序兩次,看是否向預期那樣工作。
import json
'''導入模塊json'''
def get_stored_favorite_num():
'''查看是否存在用戶喜歡的數字'''
try:
filename = 'lucky_number.json'
with open(filename) as f_obj:
favorite_number = json.load(f_obj)
except FileNotFoundError:
return None
else:
return favorite_number
def get_favorite_number():
#如果文件不存在,則讓用戶重新輸入並存儲它
favorite_number = input("Please input your favorite number: ")
filename = "lucky_number.json"
with open(filename,'w') as f_obj:
json.dump(favorite_number,f_obj)
return favorite_number
def favorite_num():
#返回用戶新歡的數字
favorite_number = get_stored_favorite_num()
if favorite_number:
print("I know your favorite number ! It's " + str(favorite_number) + " .")
else:
favorite_number = get_favorite_number()
print("I'll remember your favorite number: " + str(favorite_number))
favorite_num()
10-13 驗證用戶:最后一個remember_me.py版本假設用戶要么已輸入其用戶名,要么是首次運行該程序。我們應修改這個程序,以應對這樣的情形:
當前和最后一次運行該程序的用戶並非同一個人。
為此,在greet_user()中打印歡迎回來的消息前,先詢問他用戶名是否是對的。如果不對,就調用get_new_username()讓用戶輸入正確的用戶名。
import json
def get_stored_username():
'''如果存儲了用戶名,就獲取它'''
try:
filename = 'username.json'
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
return None
else:
while True:
#判斷用戶名是否相同,這里要求用戶輸入相同的名字我們才歡迎他,不然會一直讓用戶輸入
yourname = input("Enter your username: ")
if username == yourname:
return username
break
else:
print("\nThe user name you entered is incorrect, please re-enter.")
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()