Python書籍閱讀與記錄 6.17 I 文件和異常

我感覺這樣記錄,對於我來說挺好的。因爲我看兩端對齊的語句容易走神,這樣記錄閱讀的話,就很少出現之前的情況。

我寫的初衷,也是自己來看,所以感覺寫的不好的,請保留下意見,謝謝。

代碼縮進情況,字體重複情況,因爲我能看懂,就沒改。

 

 

裏面的每一個字我都看過,加粗 括號  下劃線 等均是我的筆記。
 

 
第10章 文件和異常
至此,你掌握了編寫組織有序而易於使用的程序所需的基本技能,該考慮讓程序目標更明確、用途更大了。在本章中,你將學習處理文件,讓程序能夠快速地分析大
量的數據;你將學習錯誤處理,避免程序在面對意外情形時崩潰;你將學習異
常 ,它們是Python創建的特殊對象,用於管理程序運行時出現的錯誤;你還將學習模
json ,它讓你能夠保存用戶數據,以免在程序停止運行後丟失。
學習處理文件和保存數據可讓你的程序使用起來更容易:用戶將能夠選擇輸入什麼樣的數據,以及在什麼時候輸入;用戶使用你的程序做一些工作後,可將程序關
閉,以後再接着往下做。學習處理異常可幫助你應對文件不存在的情形,以及處理其他可能導致程序崩潰的問題。這讓你的程序在面對錯誤的數據時更健壯——不管
這些錯誤數據源自無意的錯誤,還是源自破壞程序的惡意企圖。你在本章學習的技能可提高程序的適用性、可用性和穩定性。
10.1 從文件中讀取數據
文本文件可存儲的數據量多得難以置信:天氣數據、交通數據、社會經濟數據、文學作品等。每當需要分析或修改存儲在文件中的信息時,讀取文件都很有用,對數據分析應用
程序來說尤其如此。例如,你可以編寫一個這樣的程序:讀取一個文本文件的內容,重新設置這些數據的格式並將其寫入文件,讓瀏覽器能夠顯示這些內容。
要使用文本文件中的信息,首先需要將信息讀取到內存中。爲此,你可以一次性讀取文件的全部內容,也可以以每次一行的方式逐步讀取。
10.1.1 讀取整個文件
要讀取文件,需要一個包含幾行文本的文件。下面首先來創建一個文件,它包含精確到小數點後30位的圓周率值,且在小數點後每10位處都換行:
pi_digits.txt
3.1415926535
8979323846
2643383279
要動手嘗試後續示例,可在編輯器中輸入這些數據行,再將文件保存爲pi_digits.txt。然後,將該
文件保存到本章程序所在的目錄中。
下面的程序打開並讀取這個文件,再將其內容顯示到屏幕上:
file_reader.py
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
在這個程序中,第1行代碼做了大量的工作。我們先來看看函數open() 。要以任何方式使用文件——哪怕僅僅是打印其內容,都得先打
開 文件,這樣才能訪問它。函數open()
接受一個參數:要打開的文件的名稱。Python在當前執行的文件所在的目錄中查找指定的文件。在這個示例中,當前運行的是file_reader.py,因此Pythonfile_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.txt的文件對象後,我們使用方法read() (前述程序的第2行)讀取這個文件的全部內容,並將其作爲一個長長的字符串存儲在變量contents 中。這樣,通過
打印contents 的值,就可將這個文本文件的全部內容顯示出來:
3.1415926535
8979323846
2643383279 
 
#我這裏寫的代碼 直接在命令行敲的。但是沒有發現作者所說的最後一行多了個空行這樣的問題。
>>> import os
>>> os.getcwd()
'C:\\Users\\42152'
>>> with open('111.txt') as file_object:
...  contents=file_object.read()
...  print(contents)
...
3.14
15
92
65
3
>>> print(contents.rstrip())
3.14
15
92
65
3
>>> contents
'3.14\n15\n92\n65\n3'
>>> contents.rstrip()
'3.14\n15\n92\n65\n3'
>>> print(file_object.read())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
我經過測試還發現了:
>>> with open('111.txt'):
...  print(open('111.txt').read())
...  print('I love china')
...
3.14
15
92
6
I love china
 
>>> with open('111.txt') as file_object:
...  print(file_object.read())
...  print('I love china')
...
3.14
15
92
6
I love china
由此可以看出來,這個with語句不是for循環,只起到不用即關(文件)的作用,read()函數可以逐行一次性讀完文件內容。
 
相比於原始文件,該輸出唯一不同的地方是末尾多了一個空行。爲何會多出這個空行呢?因爲read() 到達文件末尾時返回一個空字符串,而將這個空字符串顯示出來時就是一
個空行。要刪除多出來的空行,可在print 語句中使用rstrip()
with open('pi_digits.txt') 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到指定的位置去查找,而該位置是相對於當前運行的程
序所在目錄的。LinuxOS X中,你可以這樣編寫代碼:
with open('text_files/filename.txt') as file_object:
這行代碼讓Python到文件夾python_work下的文件夾text_files中去查找指定的.txt文件。Windows系統中,在文件路徑中使用反斜槓(\ )而不是斜槓(/ ):
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() 會有所幫助。在LinuxOS 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)中。
Windows系統有時能夠正確地解讀文件路徑中的斜槓。如果你使用的是Windows系統,且結果不符合預期,請確保在文件路徑中使用的是反斜槓。
10.1.3 逐行讀取
讀取文件時,常常需要檢查其中的每一行:你可能要在文件中查找特定的信息,或者要以某種方式修改文件中的文本。例如,你可能要遍歷一個包含天氣數據的文件,並使用天
氣描述中包含字樣sunny的行。在新聞報道中,你可能會查找包含標籤<headline> 的行,並按特定的格式設置它。
要以每次一行的方式檢查文件,可對文件對象使用for 循環:
file_reader.py
filename = 'pi_digits.txt'
with open(filename) as file_object:
for line in file_object:
print(line)
 

>>> with open('111.txt') as f2:
...  for line in f2:
...   print(f2)
...   print(line)
...
<_io.TextIOWrapper name='111.txt' mode='r' encoding='cp936'>
3.14

<_io.TextIOWrapper name='111.txt' mode='r' encoding='cp936'>
15

<_io.TextIOWrapper name='111.txt' mode='r' encoding='cp936'>
92

<_io.TextIOWrapper name='111.txt' mode='r' encoding='cp936'>
6

 
處,我們將要讀取的文件的名稱存儲在變量filename 中,這是使用文件時一種常見的做法。由於變量filename 表示的並非實際文件——它只是一個讓Python知道到哪裏
 
>>> with open('111.txt') as f1:
...  print(f1)
...
<_io.TextIOWrapper name='111.txt' mode='r' encoding='cp936'>
 
去查找文件的字符串,因此可輕鬆地將'pi_digits.txt' 替換爲你要使用的另一個文件的名稱。調用open() 後,將一個表示文件及其內容的對象存儲到了變
file_object 中(見)。這裏也使用了關鍵字with ,讓Python負責妥善地打開和關閉文件。爲查看文件的內容,我們通過對文件對象執行循環來遍歷文件中的每一行(見
)。
我們打印每一行時,發現空白行更多了:
3.1415926535
8979323846
2643383279
爲何會出現這些空白行呢?因爲在這個文件中,每行的末尾都有一個看不見的換行符,而print 語句也會加上一個換行符,因此每行末尾都有兩個換行符:一個來自文件,另一
個來自print 語句。要消除這些多餘的空白行,可在print 語句中使用rstrip()
filename = 'pi_digits.txt'
with open(filename) 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.txt的各行存儲在一個列表中,再在with 代碼塊外打印它們:
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())
處的方法readlines() 從文件中讀取每一行,並將其存儲在一個列表中;接下來,該列表被存儲到變量lines 中;在with 代碼塊外,我們依然可以使用這個變量。在
處,我們使用一個簡單的for 循環來打印lines 中的各行。由於列表lines 的每個元素都對應於文件中的一行,因此輸出與文件內容完全一致。
10.1.5 使用文件的內容
將文件讀取到內存中後,就可以以任何方式使用這些數據了。下面以簡單的方式使用圓周率的值。首先,我們將創建一個字符串,它包含文件中存儲的所有數字,且沒有任何空
格:
pi_string.py
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.rstrip()
print(pi_string)
print(len(pi_string))
就像前一個示例一樣,我們首先打開文件,並將其中的所有行都存儲在一個列表中。在處,我們創建了一個變量——pi_string ,用於存儲圓周率的值。接下來,我們使用
一個循環將各行都加入pi_string ,並刪除每行末尾的換行符(見。在處,我們打印這個字符串及其長度:
3.1415926535 89793238462643383279
36
在變量pi_string 存儲的字符串中,包含原來位於每行左邊的空格,爲刪除這些空格,可使用strip() 而不是rstrip()
filename = 'pi_30_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)
print(len(pi_string))
這樣,我們就獲得了一個這樣的字符串:它包含精確到30位小數的圓周率值。這個字符串長32字符,因爲它還包含整數部分的3和小數點:
3.141592653589793238462643383279
32
讀取文本文件時,Python將其中的所有文本都解讀爲字符串。如果你讀取的是數字,並要將其作爲數值使用,就必須使用函數int() 將其轉換爲整數,或使用
函數float() 將其轉換爲浮點數
10.1.6 包含一百萬位的大型文件
前面我們分析的都是一個只有三行的文本文件,但這些代碼示例也可處理大得多的文件。如果我們有一個文本文件,其中包含精確到小數點後1 000 000位而不是30位的圓周率
值,也可創建一個包含所有這些數字的字符串。爲此,我們無需對前面的程序做任何修改,只需將這個文件傳遞給它即可。在這裏,我們只打印到小數點後50位,以免終端爲顯
示全部1 000 000位而不斷地翻滾:
pi_string.py
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))
輸出表明,我們創建的字符串確實包含精確到小數點後1 000 000位的圓周率值:
3.14159265358979323846264338327950288419716939937510...
1000002
對於你可處理的數據量,Python沒有任何限制;只要系統的內存足夠多,你想處理多少數據都可以。  
10.1.7 圓周率值中包含你的生日麼
我一直想知道自己的生日是否包含在圓周率值中。下面來擴展剛纔編寫的程序,以確定某個人的生日是否包含在圓周率值的前1 000 000位中。爲此,可將生日表示爲一個由數字
組成的字符串,再檢查這個字符串是否包含在pi_string 中:
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.rstrip()
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.")
處,我們提示用戶輸入其生日,在接下來的處,我們檢查這個字符串是否包含在pi_string 中。運行一下這個程序:Enter your birthdate, in the form mmddyy: 120372
Your birthday appears in the first million digits of pi!
我的生日確實出現在了圓周率值中!讀取文件的內容後,就可以以你能想到的任何方式對其進行分析。
動手試一試
 
10-1 Python
學習
習筆
筆記
記 :在文本編輯器中新建一個文件,寫幾句話來總結一下你至此學到的Python知識,其中每一行都以“In Python you can”打頭。將這個文件命名爲
learning_python.txt,並將其存儲到爲完成本章練習而編寫的程序所在的目錄中。編寫一個程序,它讀取這個文件,並將你所寫的內容打印三次:第一次打印時讀取整個
文件;第二次打印時遍歷文件對象;第三次打印時將各行存儲在一個列表中,再在with 代碼塊外打印它們。
10-2 C
語言
言學
學習
習筆
筆記
記 :可使用方法replace() 將字符串中的特定單詞都替換爲另一個單詞。下面是一個簡單的示例,演示瞭如何將句子中的'dog' 替換爲'cat'
>>> message = "I really like dogs."
>>> message.replace('dog', 'cat')
'I really like cats.'
讀取你剛創建的文件learning_python.txt中的每一行,將其中的Python都替換爲另一門語言的名稱,如C。將修改後的各行都打印到屏幕上。
10.2 寫入文件
保存數據的最簡單的方式之一是將其寫入到文件中。通過將輸出寫入文件,即便關閉包含程序輸出的終端窗口,這些輸出也依然存在:你可以在程序結束運行後查看這些輸出,
可與別人分享輸出文件,還可編寫程序來將這些輸出讀取到內存中並進行處理。
10.2.1 寫入空文件
要將文本寫入文件,你在調用open() 時需要提供另一個實參,告訴Python你要寫入打開的文件。爲明白其中的工作原理,我們來將一條簡單的消息存儲到文件中,而不是將其打
印到屏幕上:
write_message.py
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.")
在這個示例中,調用open() 時提供了兩個實參(見)。第一個實參也是要打開的文件的名稱;第二個實參('w' )告訴Python,我們要以寫入模式 打開這個文件。打開文件
時,可指定讀取模式 ('r' )、寫入模式 ('w' )、附加模
式 ('a' )或讓你能夠讀取和寫入文件的模式('r+' )。如果你省略了模式實參,Python將以默認的只讀模式打
開文件。
如果你要寫入的文件不存在,函數open() 將自動創建它。然而,以寫入('w' )模式打開文件時千萬要小心,因爲如果指定的文件已經存在,Python將在返回文件對象前清空
該文件。
處,我們使用文件對象的方法write() 將一個字符串寫入文件。這個程序沒有終端輸出,但如果你打開文件programming.txt,將看到其中包含如下一行內容:
programming.txt
I love programming.
相比於你的計算機中的其他文件,這個文件沒有什麼不同。你可以打開它、在其中輸入新文本、複製其內容、將內容粘貼到其中等。
Python只能將字符串寫入文本文件。要將數值數據存儲到文本文件中,必須先使用函數str() 將其轉換爲字符串格式。
10.2.2 寫入多行
函數write() 不會在你寫入的文本末尾添加換行符,因此如果你寫入多行時沒有指定換行符,文件看起來可能不是你希望的那樣:
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 = 'programming.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中再添加一些你酷愛編程的原因:
write_message.py
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")
處,我們打開文件時指定了實參'a' ,以便將內容附加到文件末尾,而不是覆蓋文件原來的內容。在處,我們又寫入了兩行,它們被添加到文件programming.txt末尾:
programming.txt
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.
最終的結果是,文件原來的內容還在,它們後面是我們剛添加的內容。
動手試一試
 
10-3
訪客
客 :編寫一個程序,提示用戶輸入其名字;用戶作出響應後,將其名字寫入到文件guest.txt中。
10-4
訪客
客名
名單
單 :編寫一個while 循環,提示用戶輸入其名字。用戶輸入其名字後,在屏幕上打印一句問候語,並將一條訪問記錄添加到文件guest_book.txt中。確保這
個文件中的每條記錄都獨佔一行。
10-5
關於
於編
編程
程的
的調
調查
查 :編寫一個while 循環,詢問用戶爲何喜歡編程。每當用戶輸入一個原因後,都將其添加到一個存儲所有原因的文件中。
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 "division.py", line 1, in <module>
print(5/0)
ZeroDivisionError: division by zero
在上述traceback中,處指出的錯誤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("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
if second_number == 'q':
break
answer = int(first_number) / int(second_number)
print(answer)
處,這個程序提示用戶輸入一個數字,並將其存儲到變量first_number 中;如果用戶輸入的不是表示退出的q,就再提示用戶輸入一個數字,並將其存儲到變
second_number 中(見)。接下來,我們計算這兩個數字的商(即answer ,見)。這個程序沒有采取任何處理錯誤的措施,因此讓它執行除數爲0的除法運算時,它
將崩潰:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division.py", line 9, in <module>
answer = int(first_number) / int(second_number)
ZeroDivisionError: 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("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)
我們讓Python嘗試執行try 代碼塊中的除法運算(見),這個代碼塊只包含可能導致錯誤的代碼。依賴於try 代碼塊成功執行的代碼都放在else 代碼塊中;在這個示例中,如
果除法運算成功,我們就使用else 代碼塊來打印結果(見)。
except 代碼塊告訴Python,出現ZeroDivisionError 異常時該怎麼辦(見)。如果try 代碼塊因除零錯誤而失敗,我們就打印一條友好的消息,告訴用戶如何避免這種錯
誤。程序將繼續運行,用戶根本看不到traceback
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First 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 "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 分析文本
你可以分析包含整本書的文本文件。很多經典文學作品都是以簡單文本文件的方式提供的,因爲它們不受版權限制。本節使用的文本來自項目Gutenberghttp://gutenberg.org/ ),
這個項目提供了一系列不受版權限制的文學作品,如果你要在編程項目中使用文學文本,這是一個很不錯的資源。
下面來提取童話 Alicein Wonderland 的文本,並嘗試計算它包含多少個單詞。我們將使用方法split() ,它根據一個字符串創建一個單詞列表。下面是對只包含童話名"Alice
in Wonderland" 的字符串調用方法split() 的結果:
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']
方法split() 以空格爲分隔符將字符串分拆成多個部分,並將這些部分都存儲到一個列表中。結果是一個包含字符串中所有單詞的列表,雖然有些單詞可能包含標點。爲計算
Alicein Wonderland 包含多少個單詞,我們將對整篇小說調用split() ,再計算得到的列表包含多少個元素,從而確定整篇童話大致包含多少個單詞:
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.")
我們把文件alice.txt移到了正確的目錄中,讓try 代碼塊能夠成功地執行。在處,我們對變量contents (它現在是一個長長的字符串,包含童話 Alicein Wonderland 的全部文
本)調用方法split() ,以生成一個列表,其中包含這部童話中的所有單詞。當我們使用len() 來確定這個列表的長度時,就知道了原始字符串大致包含多少個單詞(見
。在處,我們打印一條消息,指出文件包含多少個單詞。這些代碼都放在else 代碼塊中,因爲僅當try 代碼塊成功執行時才執行它們。輸出指出了文件alice.txt包含多少
個單詞:
The file alice.txt has about 29461 words.
這個數字有點大,因爲這裏使用的文本文件包含出版商提供的額外信息,但與童話 Alicein 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:
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)
這些代碼大都與原來一樣,我們只是將它們移到了函數count_words() 中,並增加了縮進量。修改程序的同時更新註釋是個不錯的習慣,因此我們將註釋改成了文檔字符串,
並稍微調整了一下措辭(見)。
現在可以編寫一個簡單的循環,計算要分析的任何文本包含多少個單詞了。爲此,我們將要分析的文件的名稱存儲在一個列表中,然後對列表中的每個文件都調
count_words() 。我們將嘗試計算 Alicein Wonderland Siddhartha Moby Dick Little Women 分別包含多少個單詞,它們都不受版權限制。我故意沒有將siddhartha.txt放到
word_count.py所在的目錄中,讓你能夠看到這個程序在文件不存在時處理得有多出色:
def count_words(filename):
--snip--
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
文件siddhartha.txt不存在,但這絲毫不影響這個程序處理其他文件:
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.
在這個示例中,使用try-except 代碼塊提供了兩個重要的優點:避免讓用戶看到traceback;讓程序能夠繼續分析能夠找到的其他文件。如果不捕獲因找不到siddhartha.txt而引發
FileNotFoundError 異常,用戶將看到完整的traceback,而程序將在嘗試分析 Siddhartha 後停止運行——根本不分析 Moby Dick 和 Little Women 。
10.3.8 失敗時一聲不吭
在前一個示例中,我們告訴用戶有一個文件找不到。但並非每次捕獲到異常時都需要告訴用戶,有時候你希望程序在發生異常時一聲不吭,就像什麼都沒有發生一樣繼續運行。
要讓程序在失敗時一聲不吭,可像通常那樣編寫try 代碼塊,但在except 代碼塊中明確地告訴Python什麼都不要做。Python有一個pass 語句,可在代碼塊中使用它來讓Python
什麼都不要做:
def count_words(filename):
"""計算一個文件大致包含多少個單詞"""
try:
--snip--
except FileNotFoundError:
pass
else:
--snip--
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
相比於前一個程序,這個程序唯一不同的地方是處的pass 語句。現在,出現FileNotFoundError 異常時,將執行except 代碼塊中的代碼,但什麼都不會發生。這種錯誤
發生時,不會出現traceback,也沒有任何輸出。用戶將看到存在的每個文件包含多少個單詞,但沒有任何跡象表明有一個文件未找到:
The file alice.txt has about 29461 words.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
pass 語句還充當了佔位符,它提醒你在程序的某個地方什麼都沒有做,並且以後也許要在這裏做些什麼。例如,在這個程序中,我們可能決定將找不到的文件的名稱寫入到文
missing_files.txt中。用戶看不到這個文件,但我們可以讀取這個文件,進而處理所有文件找不到的問題。
10.3.9 決定報告哪些錯誤
在什麼情況下該向用戶報告錯誤?在什麼情況下又應該在失敗時一聲不吭呢?如果用戶知道要分析哪些文件,他們可能希望在有文件沒有分析時出現一條消息,將其中的原因告
訴他們。如果用戶只想看到結果,而並不知道要分析哪些文件,可能就無需在有些文件不存在時告知他們。向用戶顯示他不想看到的信息可能會降低程序的可用性。Python的錯誤
處理結構讓你能夠細緻地控制與用戶分享錯誤信息的程度,要分享多少信息由你決定。
編寫得很好且經過詳盡測試的代碼不容易出現內部錯誤,如語法或邏輯錯誤,但只要程序依賴於外部因素,如用戶輸入、存在指定的文件、有網絡鏈接,就有可能出現異常。憑
借經驗可判斷該在程序的什麼地方包含異常處理塊,以及出現錯誤時該向用戶提供多少相關的信息。
動手試一試
 
10-6
加法
法運
運算
算 :提示用戶提供數值輸入時,常出現的一個問題是,用戶提供的是文本而不是數字。在這種情況下,當你嘗試將輸入轉換爲整數時,將引
TypeError 異常。編寫一個程序,提示用戶輸入兩個數字,再將它們相加並打印結果。在用戶輸入的任何一個值不是數字時都捕獲TypeError 異常,並打印一條友好的錯誤消息。對你編寫的程序進行測試:先輸入兩個數字,再輸入一些文本而不是數字。
10-7
加法
法計
計算
算器
器 :將你爲完成練習10-6而編寫的代碼放在一個while 循環中,讓用戶犯錯(輸入的是文本而不是數字)後能夠繼續輸入數字。
10-8
貓和
和狗
狗 :創建兩個文件cats.txtdogs.txt,在第一個文件中至少存儲三隻貓的名字,在第二個文件中至少存儲三條狗的名字。編寫一個程序,嘗試讀取這些文件,
並將其內容打印到屏幕上。將這些代碼放在一個try-except 代碼塊中,以便在文件不存在時捕獲FileNotFound 錯誤,並打印一條友好的消息。將其中一個文件
移到另一個地方,並確認except 代碼塊中的代碼將正確地執行。
10-9
沉默
默的
的貓
貓和
和狗
狗 :修改你在練習10-8中編寫的except 代碼塊,讓程序在文件不存在時一言不發。
10-10
常見
見單
單詞
詞 :訪問項目Gutenberghttp://gutenberg.org/ ),並找一些你想分析的圖書。下載這些作品的文本文件或將瀏覽器中的原始文本複製到文本文件中。
你可以使用方法count() 來確定特定的單詞或短語在字符串中出現了多少次。例如,下面的代碼計算'row' 在一個字符串中出現了多少次:
>>> line = "Row, row, row your boat"
>>> line.count('row')
2
>>> line.lower().count('row')
3
請注意,通過使用lower() 將字符串轉換爲小寫,可捕捉要查找的單詞出現的所有次數,而不管其大小寫格式如何。
編寫一個程序,它讀取你在項目Gutenberg中獲取的文件,並計算單詞'the' 在每個文件中分別出現了多少次。
10.4 存儲數據
很多程序都要求用戶輸入某種信息,如讓用戶存儲遊戲首選項或提供要可視化的數據。不管專注的是什麼,程序都把用戶提供的信息存儲在列表和字典等數據結構中。用戶關閉
程序時,你幾乎總是要保存他們提供的信息;一種簡單的方式是使用模塊json 來存儲數據。
模塊json 讓你能夠將簡單的Python數據結構轉儲到文件中,並在程序再次運行時加載該文件中的數據。你還可以使用json Python程序之間分享數據。更重要的是,JSON數據
格式並非Python專用的,這讓你能夠將以JSON格式存儲的數據與使用其他編程語言的人分享。這是一種輕便格式,很有用,也易於學習。
JSONJavaScript Object Notation)格式最初是爲JavaScript開發的,但隨後成了一種常見格式,被包括Python在內的衆多語言採用。
10.4.1 使用json.dump()和json.load()  #dump 轉儲  
我們來編寫一個存儲一組數字的簡短程序,再編寫一個將這些數字讀取到內存中的程序。第一個程序將使用json.dump() 來存儲這組數字,而第二個程序將使
json.load()
函數json.dump() 接受兩個實參:要存儲的數據以及可用於存儲數據的文件對象。下面演示瞭如何使用json.dump() 來存儲數字列表:
number_writer.py
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 ,再創建一個數字列表。在處,我們指定了要將該數字列表存儲到其中的文件的名稱。通常使用文件擴展名.json來指出文件存儲的數據爲JSON格式。接
下來,我們以寫入模式打開這個文件,讓json 能夠將數據寫入其中(見)。在處,我們使用函數json.dump() 將數字列表存儲到文件numbers.json中。
這個程序沒有輸出,但我們可以打開文件numbers.json,看看其內容。數據的存儲格式與Python中一樣:
[2, 3, 5, 7, 11, 13]
下面再編寫一個程序,使用json.load() 將這個列表讀取到內存中:
number_reader.py
import json
filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)
print(numbers)
處,我們確保讀取的是前面寫入的文件。這次我們以讀取方式打開這個文件,因爲Python只需讀取這個文件(見)。在處,我們使用函數json.load() 加載存儲在
numbers.json中的信息,並將其存儲到變量numbers 中。最後,我們打印恢復的數字列表,看看它是否與number_writer.py中創建的數字列表相同:
[2, 3, 5, 7, 11, 13]
這是一種在程序之間共享數據的簡單方式。
10.4.2 保存和讀取用戶生成的數據
對於用戶生成的數據,使用json 保存它們大有裨益,因爲如果不以某種方式進行存儲,等程序停止運行時用戶的信息將丟失。下面來看一個這樣的例子:用戶首次運行程序時
被提示輸入自己的名字,這樣再次運行程序時就記住他了。
我們先來存儲用戶的名字:remember_me.py
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 + "!")
處,我們提示輸入用戶名,並將其存儲在一個變量中。接下來,我們調用json.dump() ,並將用戶名和一個文件對象傳遞給它,從而將用戶名存儲到文件中(見)。然
後,我們打印一條消息,指出我們存儲了他輸入的信息(見):
What is your name? Eric
We'll remember you when you come back, Eric!
現在再編寫一個程序,向其名字被存儲的用戶發出問候:
greet_user.py
import json
filename = 'username.json'
with open(filename) as f_obj:
username = json.load(f_obj)
print("Welcome back, " + username + "!")
處,我們使用json.load() 將存儲在username.json中的信息讀取到變量username 中。恢復用戶名後,我們就可以歡迎用戶回來了(見):
Welcome back, Eric!
我們需要將這兩個程序合併到一個程序(remember_me.py)中。這個程序運行時,我們將嘗試從文件username.json中獲取用戶名,因此我們首先編寫一個嘗試恢復用戶名的try
碼塊。如果這個文件不存在,我們就在except 代碼塊中提示用戶輸入用戶名,並將其存儲在username.json中,以便程序再次運行時能夠獲取它:
remember_me.py
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 + "!")
這裏沒有任何新代碼,只是將前兩個示例的代碼合併到了一個程序中。在處,我們嘗試打開文件username.json如果這個文件存在,就將其中的用戶名讀取到內存中(見),
再執行else 代碼塊,即打印一條歡迎用戶回來的消息。用戶首次運行這個程序時,文件username.json不存在,將引發FileNotFoundError 異常(見),因此Python將執
except 代碼塊:提示用戶輸入其用戶名(見),再使用json.dump() 存儲該用戶名,並打印一句問候語(見)。
無論執行的是except 代碼塊還是else 代碼塊,都將顯示用戶名和合適的問候語。
如果這個程序是首次運行,輸出將如下:
What is your name? Eric
We'll remember you when you come back, Eric!
否則,輸出將如下:
Welcome back, Eric!
這是程序之前至少運行了一次時的輸出。
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, 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()
 
remember_me.py的這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user() ,它打印一條合適的消息:要麼歡迎老用戶回來,要麼問候新用戶。爲此,
它首先調用get_stored_username() ,這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username() ,這個函數只負責獲取並存儲新
用戶的用戶名。要編寫出清晰而易於維護和擴展的代碼,這種劃分工作必不可少。
動手試一試
 
10-11
喜歡
歡的
的數
數字
字 :編寫一個程序,提示用戶輸入他喜歡的數字,並使用json.dump() 將這個數字存儲到文件中。再編寫一個程序,從文件中讀取這個值,並打印
消息“I knowyour favorite number! It's _____.”
10-12
記住
住喜
喜歡
歡的
的數
數字
字 :將練習10-11中的兩個程序合而爲一。如果存儲了用戶喜歡的數字,就向用戶顯示它,否則提示用戶輸入他喜歡的數字並將其存儲到文件中。
運行這個程序兩次,看看它是否像預期的那樣工作。
10-13
驗證
證用
用戶
戶 :最後一個remember_me.py版本假設用戶要麼已輸入其用戶名,要麼是首次運行該程序。我們應修改這個程序,以應對這樣的情形:當前和最後一次
運行該程序的用戶並非同一個人。
爲此,在greet_user() 中打印歡迎用戶回來的消息前,先詢問他用戶名是否是對的。如果不對,就調用get_new_username() 讓用戶輸入正確的用戶名。
10.5 小結
在本章中,你學習了:如何使用文件;如何一次性讀取整個文件,以及如何以每次一行的方式讀取文件的內容;如何寫入文件,以及如何將文本附加到文件末尾;什麼是異常以
及如何處理程序可能引發的異常;如何存儲Python數據結構,以保存用戶提供的信息,避免用戶每次運行程序時都需要重新提供。在第11章中,你將學習高效的代碼測試方式,這可幫助你確定代碼正確無誤,以及發現擴展現有程序時可能引入的bug
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章