python學習要點2
python學習記錄
注意 :(以及後面的眾多示例),你需要從 https://www.nostarch.com/pythoncra-shcourse/下載相關的資源
第9章 類
9.1 建立和使用類
9.1.1 建立Dog類
class Dog():
"""一次模擬狗的簡單嘗試"""
def __init__(self, name, age):
"""初始化屬性name和age"""
self.name = name
self.age = age
def sit(self):
"""模擬小狗被命令時蹲下"""
print(self.name.title() + " is now sitting.")
def roll_over(self):
"""模擬小狗被命令時打滾"""
print(self.name.title() + " rolled over!")
- 方法__init__(): 每當根據類建立新例項時,Python會自動執行它,方法的名稱中,開頭和末尾各有兩個下劃線,這是一種約定,包含三個引數,self必不可少,還必須位於其它形參的前面。
- 通常可以認為首字母大寫的名稱指的是類,而小寫的名稱指的是根據類建立的事例。
- 訪問屬性
要訪問例項的屬性,可使用句點表示法。
9.2 使用類和例項
9.2.2 給屬性指定預設值
類中的每個屬性都必須有初始值,
9.2.3 修改屬性的值
-
直接修改屬性的值
-
通過方法修改屬性的值
-
通過方法對屬性的值進行遞增
9.3 繼承
建立子類的例項時,Python首先需要完成的任務是給父類的所有屬性賦值。為此,子類的方法__init__()需要父類施以援手。
super().init();
class Car():
"""一次模擬汽車的簡單嘗試"""
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
def read_odometer(self):
print("This car has " + str(self.odometer_reading) + " miles on it.")
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
"""
電動汽車的獨特之處
初始化父類的屬性,再初始化電動汽車特有的屬性
"""
super().__init__(make, model, year)
self.battery_size = 70
def describe_battery(self):
"""列印一條描述電瓶容量的訊息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
9.3.3 給子類定義屬性和方法
class Car():
-- snip --
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
"""
電動汽車的獨特之處
初始化父類的屬性,再初始化電動汽車特有的屬性
"""
super().__init__(make, model, year)
self.battery_size = 70
def describe_battery(self):
"""列印一條描述電瓶容量的訊息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
9.3.4 重寫父類的方法
def ElectricCar(Car):
-- snip --
def fill_gas_tank():
"""電動汽車沒有油箱"""
print("This car doesn't need a gas tank!")
9.3.5 將例項用作屬性
當類的屬性和方法清單越來越多時,可能需要將類的一部分作為一個獨立的類提取出來。
class Car():
-- snip --
class Battery():
"""一次模擬電動汽車電瓶的簡單嘗試"""
def __init__(self, battery_size=70):
"""初始化電瓶的屬性"""
self.battery_size = battery_size
def describe_battery(self):
"""列印一條描述電瓶容量的訊息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
class ElectricCar(Car):
"""電動汽車的獨特之處"""
def __init__(self, make, model, year):
"""
初始化父類的屬性,再初始化電動汽車特有的屬性
"""
super().__init__(make, model, year)
self.battery = Battery()
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
9.4 匯入類
"""一個可用於表示汽車的類"""
class Car():
"""一次模擬汽車的簡單嘗試"""
def __init__(self, make, model, year):
"""初始化描述汽車的屬性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""返回整潔的描述性名稱"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
def read_odometer(self):
"""列印一條訊息,指出汽車的里程"""
print("This car has " + str(self.odometer_reading) + " miles on it.")
def update_odometer(self, mileage):
"""
將里程錶讀數設定為指定的值
拒絕將里程錶往回撥
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
"""將里程錶讀數增加指定的量"""
self.odometer_reading += miles
文件包含了一個模組級文件字串,對該模組的內容做了簡要的描述。應該為自己建立的每個模組都編寫文件字串
my_car.py
from car import Car
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
9.4.2 在一個模組中儲存多個類
"""一組用於表示燃油汽車和電動汽車的類"""
class Car():
-- snip --
class Battery():
"""一次模擬電動汽車電瓶的簡單嘗試"""
def __init__(self, battery_size=60):
"""初始化電瓶的屬性"""
self.battery_size = battery_size
def describe_battery(self):
"""列印一條描述電瓶容量的訊息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
def get_range(self):
"""列印一條描述電瓶續航里程的訊息"""
if self.battery_size == 70:
range = 240
elif self.battery_size == 85:
range = 270
message = "This car can go approximately " + str(range)
message += " miles on a full charge."
print(message)
class ElectricCar(Car):
"""模擬電動汽車的獨特之處"""
def __init__(self, make, model, year):
"""
初始化父類的屬性,再初始化電動汽車特有的屬性
"""
super().__init__(make, model, year)
self.battery = Battery()
現在,可以新建一個名為my_electric_car.py的檔案,匯入 ElectricCar 並建立一輛電動汽車了:
my_electric_car.py
from car import ElectricCar
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
輸出與我們前面看到的相同,但大部分邏輯都隱藏在一個模組中:
9.4.3 從一個模組中匯入多個類
my_cars.py
from car import Car, ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())
9.4.4 匯入整個模組
你還可以匯入整個模組,再使用句點表示法訪問需要的類。這種匯入方法很簡單,程式碼也易於閱讀。由於建立類例項的程式碼都包含模組名,因此不會與當前檔案使用的任何名稱發生衝突。下面的程式碼匯入整個 car 模組,並建立一輛普通汽車和一輛電動汽車:
my_cars.py
import car
my_beetle = car.Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())
my_tesla = car.ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())
9.4.6 在一個模組中匯入另一個模組
有時候,需要將類分散到多個模組中,以免模組太大,或在同一個模組中儲存不相關的類。將類儲存在多個模組中時,你可能會發現一個模組中的類依賴於另一個模組中的類。在這種情況下,可在前一個模組中匯入必要的類。
例如,下面將 Car 類儲存在一個模組中,並將 ElectricCar 和 Battery 儲存在另一個模組中。
我們將第二個模組命名為 electric_car.py (這將覆蓋前面建立的檔案 electric_car.py ),並將Battery 和 ElectricCar 類複製到這個模組中:
electric_car.py
"""一組可用於表示電動汽車的類"""
from car import Car
class Battery():
-- snip --
class ElectricCar(Car):
-- snip --
ElectricCar 類需要訪問其父類 Car ,因此,我們直接將 Car 類匯入該模組中。如果我們忘記了這行程式碼,Python將在我們試圖建立 ElectricCar例項時引發錯誤。我們還需要更新模組car ,使其包含 Car 類:
car.py
"""一個可用於表示汽車的類"""
class Car():
-- snip --
現在可以分別從每個模組中匯入類,以根據需要建立任何型別的汽車了:
my_cars.py
from car import Car
from electric_car import ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())
my_tesla = ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())
9.6 類編碼風格
你必須熟悉有些與類相關的編碼風格問題,在你編寫的程式較複雜時尤其如此。類名應採用駝峰命名法,即將類名中的每個單詞的首字母都大寫,而不使用下劃線。例項名和模組名都採用小寫格式,並在單詞之間加上下劃線。
對於每個類,都應緊跟在類定義後面包含一個文件字串。這種文件字串簡要地描述類的功能,並遵循編寫函式的文件字串時採用的格式約定。每個模組也都應包含一個文件字串,對其中的類可用於做什麼進行描述。
可使用空行來組織程式碼,但不要濫用。在類中,可使用一個空行來分隔方法;而在模組中,可使用兩個空行來分隔類。
需要同時匯入標準庫中的模組和你編寫的模組時,先編寫匯入標準庫模組的 import 語句,再新增一個空行,然後編寫匯入你自己編寫的模組的 import 語句。在包含多條 import 語句的程式中,這種做法讓人更容易明白程式使用的各個模組都來自何方。
第10章 檔案和異常
10.1 從檔案中讀取資料
10.1.1 讀取整個檔案
pi_digits.txt
3.1415926535
8979323846
2643383279
ile_reader.py
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
關鍵字 with 在不再需要訪問檔案後將其關閉。在這個程式中,注意到我們呼叫了 open() ,但沒有呼叫 close() ;你也可以呼叫 open() 和 close() 來開啟和關閉檔案,但這樣做時,如果程式存在bug,導致 close() 語句未執行,檔案將不會關閉。這看似微不足道,但未妥善地關閉檔案可能會導致資料丟失或受損。如果在程式中過早地呼叫 close() ,你會發現需要使用檔案時它已關閉(無法訪問),這會導致更多的錯誤。並非在任何情況下都能輕鬆確定關閉檔案的恰當時機,但通過使用前面所示的結構,可讓Python去確定:你只管開啟檔案,並在需要時使用它,Python自會在合適的時候自動將其關閉。
相比於原始檔案,該輸出唯一不同的地方是末尾多了一個空行。為何會多出這個空行呢?因為 read() 到達檔案末尾時返回一個空字串,而將這個空字串顯示出來時就是一個空行。要刪除多出來的空行,可在 print 語句中使用 rstrip() :
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents.rstrip())
本書前面說過,Python方法 rstrip() 刪除(剝除)字串末尾的空白。
10.1.2 檔案路徑
由於資料夾text_files位於資料夾python_work中,因此可使用相對檔案路徑來開啟該資料夾中的檔案。相對檔案路徑讓Python到指定的位置去查詢,而該位置是相對於當前執行的程式所在目錄的。
這行程式碼讓Python到資料夾python_work下的資料夾text_files中去查詢指定的.txt檔案。在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:
10.1.3 逐行讀取
file_reader.py
filename = ‘pi_digits.txt’
with open(filename) as file_object:
for line in file_object:
print(line)
為何會出現 空白行呢?因為在這個檔案中,每行的末尾都有一個看不見的換行符,而print 語句也會加上一個換行符,因此每行末尾都有兩個換行符:一個來自檔案,另一個來自 print語句。要消除這些多餘的空白行,可在 print 語句中使用 rstrip() :
filename = ‘pi_digits.txt’
with open(filename) as file_object:
for line in file_object:
print(line.rstrip())
#### 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 使用檔案的內容
注意 讀取文字檔案時,Python將其中的所有文字都解讀為字串。如果你讀取的是數字,並要將其作為數值使用,就必須使用函式 int() 將其轉換為整數,或使用函式 float() 將其轉換為浮點數。
注意 要執行這個程式(以及後面的眾多示例),你需要從https://www.nostarch.com/pythoncra-shcourse/下載相關的資源
#### 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.”)
### 10.2 寫入檔案
#### 10.2.1 寫入空檔案
將一條資訊儲存到檔案中
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將在返回檔案物件前清空該檔案。
**注意 Python只能將字串寫入文字檔案。要將數值資料儲存到文字檔案中,必須先使用函式
str() 將其轉換為字串格式。**
#### 10.2.2 寫入多行
函式write()不會再寫入的文字末尾新增換行符。要讓每個字串都單獨佔一行,需新增:
file_obj.write(’\n’)
#### 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”)
### 10.3 異常
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
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)
#### 10.3.4 else 程式碼塊
通過將可能引發錯誤的程式碼放在 try-excep t程式碼塊中,可提高這個程式抵禦錯誤的能力。錯誤是執行除法運算的程式碼行導致的,因此我們需要將它放到 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 程式碼塊因除零錯誤而失敗,我們就列印一條友好的訊息,告訴使用者如何避免這種錯誤。
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
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 分析文字
你可以分析包含整本書的文字檔案。很多經典文學作品都是以簡單文字檔案的方式提供的,因為它們不受版權限制。本節使用的文字來自專案Gutenberg(http://gutenberg.org/),這個專案提供了一系列不受版權限制的文學作品,如果你要在程式設計專案中使用文學文字,這是一個很不錯的資源。
下面來提取童話Alice in Wonderland的文字,並嘗試計算它包含多少個單詞。我們將使用方法 split() ,它根據一個字串建立一個單詞列表。
方法 split() 以空格為分隔符將字串分拆成多個部分,並將這些部分都儲存到一個列表中。結果是一個包含字串中所有單詞的列表,雖然有些單詞可能包含標點。為計算Alice in 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 (它現在是一個長長的字串,包含童話Alice in Wonderland的全部文字)呼叫方法split() ,以生成一個列表,其中包含這部童話中的所有單詞。當我們使用 len() 來確定這個列表的長度時,就知道了原始字串大致包含多少個單詞。我們列印一條訊息,指出檔案包含多少個單詞。這些程式碼都放在 else 程式碼塊中,因為僅當 try 程式碼塊成功執行時才執行它們。輸出指出了檔案alice.txt包含多少個單詞:
The file alice.txt has about 29461 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:
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() 。我們將嘗試計算Alice in 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不存在,但這絲毫不影響這個程式處理其他檔案
#### 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:
相比於前一個程式,這個程式唯一不同的地方是 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.4 儲存資料
模組json讓你能夠將python資料結構資料儲存到檔案中,並在程式再次執行時載入該檔案中的資料。
#### 使用json.dump()和json.load()
函式 json.dump() 接受兩個實參:要儲存的資料以及可用於儲存資料的檔案物件。
number_writer.py
```python
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.load()將這個列表讀取到記憶體中
number_reader.py
import json
filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)
print(numbers)
10.4.2 儲存和讀取使用者生成的資料
程式執行時,我們將嘗試從檔案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 + "!")
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() ,這個函式只負責獲取並存儲新使用者的使用者名稱。要編寫出清晰而易於維護和擴充套件的程式碼,這種劃分工作必不可少。
第11章 測試程式碼
11.1 測試函式
11.1.1 單元測試與測試用例
單元測試: python標準庫中的模組unittest提供了程式碼測試工具,單元測試用於核實函式的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函式在各種情況下的行為都符合要求,
**全覆蓋式測試用例:**包含一整套單元測試,涵蓋了各種可能的函式使用方式。
11.1.2 可通過的測試
要為函式編寫測試用例,可先匯入模組unittest以及要測試的函式,在建立一個繼承unittest.TextCase的類,並編寫一系列方法對函式行為的不同方面進行測試。
- 子類中所有以test_打頭的方法都將自動執行。
- **斷言方法:**用來核實得到的結果是否與期望的結果一致。 assertEqual()
11.1.3 不能通過的測試
11.1.4 測試未通過時怎麼辦
11.1.5 新增新測試
11.2 測試類
編寫針對類的測試。
11.2.1 各種斷言方法
表11-1 unittest Module的斷言方法
方法 | 用途 |
---|---|
assertEqual(a, b) | 核實a==b |
assertNotEqual(a, b) | 核實a != b |
assertTrue(x) | 核實x 為True |
assertFalse(x) | 核實x 為 False |
assertIn(item, list) | 核實item在list中 |
assertNotIn(item, list) | 核實item不在list中 |