Python基础入门知识(11)
接前面的文章:
Python基礎入門教學
- 2 Python的基礎知識
- 2.15 文件和異常
- 2.15.4 存儲數據
- 2.15.4.1 使用json.dump()和json.load()
- 2.15.4.2 保存和讀取用戶生成的數據
- 2.15.4.3 重構
- 2.16 測試代碼
- 2.16.1 測試函數
- 2.16.1.1 單元測試和測試用例
- 2.16.1.2 可通過的測試
- 2.16.1.3 不能通過的測試
- 2.16.1.4 測試未通過時怎么辦
- 2.16.1.5 添加新測試
2 Python的基礎知識
2.15 文件和異常
2.15.4 存儲數據
很多程序都要求用戶輸入某種信息,如讓用戶存儲游戲首選項或提供要可視化的數據。不管專注的是什么,程序都把用戶提供的信息存儲在列表和字典等數據結構中。用戶關閉程序時,我們幾乎總是要保存他們提供的信息;一種簡單的方式是使用模塊json來存儲數據。
模塊json讓我們能夠將簡單的Python數據結構轉儲到文件中,并在程序再次運行時加載該文件中的數據。我們還可以使用json在Python程序之間分享數據。更重要的是,JSON數據格式并非Python專用的,這讓我們能夠將以JSON格式存儲的數據與使用其他編程語言的人分享。這是一種輕便格式,很有用,也易于學習。
- 注意:JSON(JavaScript Object Notation)格式最初是為JavaScript開發的,但隨后成了一種常見格式,被包括Python在內的眾多語言采用。
2.15.4.1 使用json.dump()和json.load()
我們來編寫一個存儲一組數字的簡短程序,再編寫一個將這些數字讀取到內存中的程序。第一個程序將使用json.dump()來存儲這組數字,而第二個程序將使用json.load()。
函數json.dump()接受兩個實參:要存儲的數據以及可用于存儲數據的文件對象。下面演示了如何使用json.dump()來存儲數字列表:
number_writer.py
我們先導入模塊json,再創建一個數字列表。在?處,我們指定了要將該數字列表存儲到其中的文件的名稱。通常使用文件擴展名.json來指出文件存儲的數據為JSON格式。接下來,我們以寫入模式打開這個文件,讓json能夠將數據寫入其中(見?)。在?處,我們使用函數json.dump()將數字列表存儲到文件numbers.json中。
這個程序沒有輸出,但我們可以打開文件numbers.json,看看其內容。數據的存儲格式與Python中一樣:
下面再編寫一個程序,使用json.load()將這個列表讀取到內存中:
number_reader.py
在?處,我們確保讀取的是前面寫入的文件。這次我們以讀取方式打開這個文件,因為Python只需讀取這個文件(見?)。在?處,我們使用函數json.load()加載存儲在numbers.json中的信息,并將其存儲到變量numbers中。最后,我們打印恢復的數字列表,看看它是否與number_writer.py中創建的數字列表相同:
[2, 3, 5, 7, 11, 13]這是一種在程序之間共享數據的簡單方式。
2.15.4.2 保存和讀取用戶生成的數據
對于用戶生成的數據,使用json保存它們大有裨益,因為如果不以某種方式進行存儲,等程序停止運行時用戶的信息將丟失。下面來看一個這樣的例子:用戶首次運行程序時被提示輸入自己的名字,這樣再次運行程序時就記住他了。
我們先來存儲用戶的名字:
remember_me.py
在?處,我們提示輸入用戶名,并將其存儲在一個變量中。接下來,我們調用json.dump(),并將用戶名和一個文件對象傳遞給它,從而將用戶名存儲到文件中(見?)。然后,我們打印一條消息,指出我們存儲了他輸入的信息(見?):
Whit is your name? shirley We'll remember you when you come back, shirley!現在再編寫一個程序,向其名字被存儲的用戶發出問候:
greet_user.py
在?處,我們使用json.load()將存儲在username.json中的信息讀取到變量username中。恢復用戶名后,我們就可以歡迎用戶回來了(見?):
Welcome back,shirley!我們需要將這兩個程序合并到一個程序(remember_me.py)中。這個程序運行時,我們將嘗試從文件username.json中獲取用戶名,因此我們首先編寫一個嘗試恢復用戶名的try代碼塊。如果這個文件不存在,我們就在except代碼塊中提示用戶輸入用戶名,并將其存儲在username.json中,以便程序再次運行時能夠獲取它:
remember_me.py
這里沒有任何新代碼,只是將前兩個示例的代碼合并到了一個程序中。在?處,我們嘗試打開文件username.json。如果這個文件存在,就將其中的用戶名讀取到內存中(見?),再執行else代碼塊,即打印一條歡迎用戶回來的消息。用戶首次運行這個程序時,文件username.json不存在,將引發FileNotFoundError異常(見?),因此Python將執行except代碼塊:提示用戶輸入其用戶名(見?),再使用json.dump()存儲該用戶名,并打印一句問候語(見?)。
無論執行的是except代碼塊還是else代碼塊,都將顯示用戶名和合適的問候語。如果這個程序是首次運行,輸出將如下:
否則,輸出將如下:
Welcome back,shirley!這是程序之前至少運行了一次時的輸出。
2.15.4.3 重構
我們經常會遇到這樣的情況:代碼能夠正確地運行,但可做進一步的改進——將代碼劃分為一系列完成具體工作的函數。這樣的過程被稱為重構。重構讓代碼更清晰、更易于理解、更容易擴展。
要重構remember_me.py,可將其大部分邏輯放到一個或多個函數中。remember_me.py的重點是問候用戶,因此我們將其所有代碼都放到一個名為greet_user()的函數中:
remember_me.py
考慮到現在使用了一個函數,我們刪除了注釋,轉而使用一個文檔字符串來指出程序是做什么的(見?)。這個程序更清晰些,但函數greet_user()所做的不僅僅是問候用戶,還在存儲了用戶名時獲取它,而在沒有存儲用戶名時提示用戶輸入一個。
下面來重構greet_user(),讓它不執行這么多任務。為此,我們首先將獲取存儲的用戶名的代碼移到另一個函數中:
新增的函數get_stored_username()目標明確,?處的文檔字符串指出了這一點。如果存儲了用戶名,這個函數就獲取并返回它;如果文件username.json不存在,這個函數就返回None(見?)。這是一種不錯的做法:函數要么返回預期的值,要么返回None;這讓我們能夠使用函數的返回值做簡單測試。在?處,如果成功地獲取了用戶名,就打印一條歡迎用戶回來的消息,否則就提示用戶輸入用戶名。
我們還需將greet_user()中的另一個代碼塊提取出來:將沒有存儲用戶名時提示用戶輸入的代碼放在一個獨立的函數中:
在remember_me.py的這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user(),它打印一條合適的消息:要么歡迎老用戶回來,要么問候新用戶。為此,它首先調用get_stored_username(),這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username(),這個函數只負責獲取并存儲新用戶的用戶名。要編寫出清晰而易于維護和擴展的代碼,這種劃分工作必不可少。
2.16 測試代碼
編寫函數或類時,還可為其編寫測試。通過測試,可確定代碼面對各種輸入都能夠按要求的那樣工作。測試讓我們信心滿滿,深信即便有更多的人使用我們的程序,它也能正確地工作。在程序中添加新代碼時,我們也可以對其進行測試,確認它們不會破壞程序既有的行為。程序員都會犯錯,因此每個程序員都必須經常測試其代碼,在用戶發現問題前找出它們。
2.16.1 測試函數
要學習測試,得有要測試的代碼。下面是一個簡單的函數,它接受名和姓并返回整潔的姓名:name_function.py
def get_formatted_name(first, last):"""Generate a neatly formatted full name."""full_name = first+' '+lastreturn full_name.title()函數get_formatted_name()將名和姓合并成姓名,在名和姓之間加上一個空格,并將它們的首字母都大寫,再返回結果。為核實get_formatted_name()像期望的那樣工作,我們來編寫一個使用這個函數的程序。程序names.py讓用戶輸入名和姓,并顯示整潔的全名:
names.py
這個程序從name_function.py中導入get_formatted_name()。用戶可輸入一系列的名和姓,并看到格式整潔的全名:
Enter 'q' at any time to quit.'Please give me a first name: janis Please give me a last name: joplinNeatly formatted name: Janis Joplin.Please give me a first name: bob Please give me a last name: dylanNeatly formatted name: Bob Dylan.Please give me a first name: q從上述輸出可知,合并得到的姓名正確無誤。現在假設我們要修改get_formatted_name(),使其還能夠處理中間名。這樣做時,我們要確保不破壞這個函數處理只有名和姓的姓名的方式。為此,我們可以在每次修改get_formatted_name()后都進行測試:運行程序names.py,并輸入像JanisJoplin這樣的姓名,但這太煩瑣了。所幸Python提供了一種自動測試函數輸出的高效方式。倘若我們對get_formatted_name()進行自動測試,就能始終信心滿滿,確信給這個函數提供我們測試過的姓名時,它都能正確地工作。
2.16.1.1 單元測試和測試用例
Python標準庫中的模塊unittest提供了代碼測試工具。
單元測試用于核實函數的某個方面沒有問題;
測試用例是一組單元測試,這些單元測試一起核實函數在各種情形下的行為都符合要求。良好的測試用例考慮到了函數可能收到的各種輸入,包含針對所有這些情形的測試。
全覆蓋式測試用例包含一整套單元測試,涵蓋了各種可能的函數使用方式。對于大型項目,要實現全覆蓋可能很難。通常,最初只要針對代碼的重要行為編寫測試即可,等項目被廣泛使用時再考慮全覆蓋。
2.16.1.2 可通過的測試
創建測試用例的語法需要一段時間才能習慣,但測試用例創建后,再添加針對函數的單元測試就很簡單了。要為函數編寫測試用例,可先導入模塊unittest以及要測試的函數,再創建一個繼承unittest.TestCase的類,并編寫一系列方法對函數行為的不同方面進行測試。
下面是一個只包含一個方法的測試用例,它檢查函數get_formatted_name()在給定名和姓時能否正確地工作:
test_name_function.py
首先,我們導入了模塊unittest和要測試的函數get_formatted_name()。在?處,我們創建了一個名為NamesTestCase的類,用于包含一系列針對get_formatted_name()的單元測試。我們可隨便給這個類命名,但最好讓它看起來與要測試的函數相關,并包含字樣Test。這個類必須繼承unittest.TestCase類,這樣Python才知道如何運行我們編寫的測試。
NamesTestCase只包含一個方法,用于測試get_formatted_name()的一個方面。我們將這個方法命名為test_first_last_name(),因為我們要核實的是只有名和姓的姓名能否被正確地格式化。我們運行testname_function.py時,所有以test打頭的方法都將自動運行。在這個方法中,我們調用了要測試的函數,并存儲了要測試的返回值。在這個示例中,我們使用實參’janis’和’joplin’調用get_formatted_name(),并將結果存儲到變量formatted_name中(見?)。
在?處,我們使用了unittest類最有用的功能之一:一個斷言方法。斷言方法用來核實得到的結果是否與期望的結果一致。在這里,我們知道get_formatted_name()應返回這樣的姓名,即名和姓的首字母為大寫,且它們之間有一個空格,因此我們期望formatted_name的值為Janis Joplin。為檢查是否確實如此,我們調用unittest的方法assertEqual(),并向它傳遞formatted_name和’Janis Joplin’。代碼行self.assertEqual(formatted_name, ‘Janis Joplin’)的意思是說:“將formatted_name的值同字符串’Janis Joplin’進行比較,如果它們相等,就萬事大吉,如果它們不相等,跟我說一聲!”
代碼行unittest.main()讓Python運行這個文件中的測試。運行test_name_function.py時,得到的輸出如下:
第1行的句點表明有一個測試通過了。接下來的一行指出Python運行了一個測試,消耗的時間不到0.001秒。最后的OK表明該測試用例中的所有單元測試都通過了。上述輸出表明,給定包含名和姓的姓名時,函數get_formatted_name()總是能正確地處理。修改get_formatted_name()后,可再次運行這個測試用例。如果它通過了,我們就知道在給定Janis Joplin這樣的姓名時,這個函數依然能夠正確地處理。
2.16.1.3 不能通過的測試
測試未通過時結果是什么樣的呢?我們來修改get_formatted_name(),使其能夠處理中間名,但這樣做時,故意讓這個函數無法正確地處理像Janis Joplin這樣只有名和姓的姓名。
下面是函數get_formatted_name()的新版本,它要求通過一個實參指定中間名:
這個版本應該能夠正確地處理包含中間名的姓名,但對其進行測試時,我們發現它再也不能正確地處理只有名和姓的姓名。這次運行程序test_name_function.py時,輸出如下:
E ? ====================================================================== ERROR: test_first last_name(_main_.NamesTestCase) ? Traceback (most recent call last): ?File"test_name_function.py",line 8,in test_first_last_nameformatted name = get_formatted_name('janis','joplin') TypeError: get_formatted_name() missing 1 required positional argumen t:'last" ---------------------------------------------------------------------- Ran 1 test in 0. 000s ? FAILED(errors=1) ?其中包含的信息很多,因為測試未通過時,需要讓我們知道的事情可能有很多。第1行輸出只有一個字母E(見?),它指出測試用例中有一個單元測試導致了錯誤。接下來,我們看到NamesTestCase中的test_first_last_name()導致了錯誤(見?)。測試用例包含眾多單元測試時,知道哪個測試未通過至關重要。在?處,我們看到了一個標準的traceback,它指出函數調用get_formatted_name(‘janis’,‘joplin’)有問題,因為它缺少一個必不可少的位置實參。
我們還看到運行了一個單元測試(見?)。最后,還看到了一條消息,它指出整個測試用例都未通過,因為運行該測試用例時發生了一個錯誤(見?)。這條消息位于輸出末尾,讓我們一眼就能看到——我們可不希望為獲悉有多少測試未通過而翻閱長長的輸出。
2.16.1.4 測試未通過時怎么辦
測試未通過時怎么辦呢?如果我們檢查的條件沒錯,測試通過了意味著函數的行為是對的,而測試未通過意味著我們編寫的新代碼有錯。因此,測試未通過時,不要修改測試,而應修復導致測試不能通過的代碼:檢查剛對函數所做的修改,找出導致函數行為不符合預期的修改。
在這個示例中,get_formatted_name()以前只需要兩個實參——名和姓,但現在它要求提供名、中間名和姓。新增的中間名參數是必不可少的,這導致get_formatted_name()的行為不符合預期。就這里而言,最佳的選擇是讓中間名變為可選的。這樣做后,使用類似于Janis Joplin的姓名進行測試時,測試就會通過了,同時這個函數還能接受中間名。下面來修改get_formatted_name(),將中間名設置為可選的,然后再次運行這個測試用例。如果通過了,我們接著確認這個函數能夠妥善地處理中間名。
要將中間名設置為可選的,可在函數定義中將形參middle移到形參列表末尾,并將其默認值指定為一個空字符串。我們還要添加一個if測試,以便根據是否提供了中間名相應地創建姓名:
在get_formatted_name()的這個新版本中,中間名是可選的。如果向這個函數傳遞了中間名(if middle:),姓名將包含名、中間名和姓,否則姓名將只包含名和姓。現在,對于兩種不同的姓名,這個函數都應該能夠正確地處理。為確定這個函數依然能夠正確地處理像Janis Joplin這樣的姓名,我們再次運行test_name_function.py:
. ---------------------------------------------------------------------- Ran 1 tests in 0.000sOK現在,測試用例通過了。太好了,這意味著這個函數又能正確地處理像JanisJoplin這樣的姓名了,而且我們無需手工測試這個函數。這個函數很容易就修復了,因為未通過的測試讓我們得知新代碼破壞了函數原來的行為。
2.16.1.5 添加新測試
確定get_formatted_name()又能正確地處理簡單的姓名后,我們再編寫一個測試,用于測試包含中間名的姓名。為此,我們在NamesTestCase類中再添加一個方法:
import unittest from name_function import get_formatted_name class NamesTestCase(unittest.TestCase):"""測試name_function.py"""def test_first_last_name(self):"""能夠正確地處理像Janis Joplin這樣地姓名嗎?"""formatted_name = get_formatted_name('janis', 'joplin')self.assertEqual(formatted_name,'Janis Joplin')def test_first_last_middle_name(self):"""能夠正確地處理像Wolfgang Amadeus Mozart這樣的姓名嗎?"""formatted_name = get_formatted_name( ?'wolfgang','mozart','amadeus')self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart') unittest.main()我們將這個方法命名為test_first_last_middle_name()。方法名必須以test_打頭,這樣它才會在我們運行test_name_function.py時自動運行。這個方法名清楚地指出了它測試的是get_formatted_name()的哪個行為,這樣,如果該測試未通過,我們就會馬上知道受影響的是哪種類型的姓名。在TestCase類中使用很長的方法名是可以的;這些方法的名稱必須是描述性的,這才能讓我們明白測試未通過時的輸出;這些方法由Python自動調用,我們根本不用編寫調用它們的代碼。
為測試函數get_formatted_name(),我們使用名、姓和中間名調用它(見?),再使用assertEqual()檢查返回的姓名是否與預期的姓名(名、中間名和姓)一致。我們再次運行test_name_function.py時,兩個測試都通過了:
太好了!現在我們知道,這個函數又能正確地處理像Janis Joplin這樣的姓名了,我們還深信它也能夠正確地處理像Wolfgang Amadeus Mozart這樣的姓名。
后續更新:
總結
以上是生活随笔為你收集整理的Python基础入门知识(11)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mac 开发工具汇总
- 下一篇: python爬虫分析模拟登录时会变的参数