第 9 章 類
物件導向程式設計 是最有效的軟體編寫方法之一。在物件導向程式設計中,你編寫表示現實世界中的事物和情景的類,並基於這些類來創建物件。編寫類時,你定義一大類物件都有的通用行為。基於類創建物件 時,每個物件都自動具備這種通用行為,然後可根據需要賦予每個物件獨特的個性。使用物件導向程式設計可模擬現實情景,其逼真程度達到了令你驚訝的地步。
根據類來創建物件被稱為產生實體 ,這讓你能夠使用類的實例。在本章中,你將編寫一些類並創建其實例。你將指定可在實例中存儲什麼資訊,定義可對這些實例執行哪些操作。你還將編寫一些類來擴展既有類的功能,讓相似的類能夠高效地共用代碼。你將把自己編寫的類存儲在模組中,並在自己的程式檔中導入其他程式師編寫的類。
理解物件導向程式設計有助於你像程式師那樣看世界,還可以幫助你真正明白自己編寫的代碼:不僅是各行代碼的作用,還有代碼背後更宏大的概念。瞭解類背後的概念可培養邏輯思維,讓你能夠通過編寫程式來解決遇到的幾乎任何問題。
隨著面臨的挑戰日益嚴峻,類還能讓你以及與你合作的其他程式師的生活更輕鬆。如果你與其他程式師基於同樣的邏輯來編寫代碼,你們就能明白對方所做的工作;你編寫的程式將能被眾多合作者所理解,每個人都能事半功倍。
9.1 創建和使用類
使用類幾乎可以模擬任何東西。下面來編寫一個表示小狗的簡單類Dog ——它表示的不是特定的小狗,而是任何小狗。對於大多數寵物狗,我們都知道些什麼呢?它們都有名字和年齡;我們還知道,大多數小狗還會蹲下和打滾。由於大多數小狗都具備上述兩項資訊(名字和年齡)和兩種行為(蹲下和打滾),我們的Dog 類將包含它們。這個類讓Python知道如何創建表示小狗的物件。編寫這個類後,我們將使用它來創建表示特定小狗的實例。
9.1.1 創建Dog 類
根據Dog 類創建的每個實例都將存儲名字和年齡。我們賦予了每條小狗蹲下(sit() )和打滾(roll_over() )的能力:
dog.py
❶ 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!")
這裡需要注意的地方很多,但你也不用擔心,本章充斥著這樣的結構,你有大把的機會熟悉它。在❶處,我們定義了一個名為Dog 的類。根據約定,在Python中,首字母大寫的名稱指的是類。這個類定義中的括弧是空的,因為我們要從空白創建這個類。在❷處,我們編寫了一個文檔字串,對這個類的功能作了描述。
1. 方法__init__()
類中的函數稱為方法 ;你前面學到的有關函數的一切都適用於方法,就目前而言,唯一重要的差別是調用方法的方式。❸處的方法__init__() 是一個特殊的方法,每當你根據Dog 類創建新實例時,Python都會自動運行它。在這個方法的名稱中,開頭和末尾各有兩個底線,這是一種約定,旨在避免Python默認方法與普通方法發生名稱衝突。
我們將方法__init__() 定義成了包含三個形參:self 、name 和age 。在這個方法的定義中,形參self 必不可少,還必須位於其他形參的前面。為何必須在方法定義中包含形參self 呢?因為Python調用這個__init__() 方法來創建Dog 實例時,將自動傳入實參self 。每個與類相關聯的方法調用都自動傳遞實參self ,它是一個指向實例本身的引用,讓實例能夠訪問類中的屬性和方法。我們創建Dog 實例時,Python將調用Dog 類的方法__init__() 。我們將通過實參向Dog() 傳遞名字和年齡;self 會自動傳遞,因此我們不需要傳遞它。每當我們根據Dog 類創建實例時,都只需給最後兩個形參(name 和age )提供值。
❹處定義的兩個變數都有首碼self 。以self 為首碼的變數都可供類中的所有方法使用,我們還可以通過類的任何實例來訪問這些變數。self.name
= name 獲取存儲在形參name 中的值,並將其存儲到變數name 中,然後該變數被關聯到當前創建的實例。self.age =
age 的作用與此類似。像這樣可通過實例訪問的變數稱為屬性 。
Dog 類還定義了另外兩個方法:sit() 和roll_over() (見❺)。由於這些方法不需要額外的資訊,如名字或年齡,因此它們只有一個形參self 。我們後面將創建的實例能夠訪問這些方法,換句話說,它們都會蹲下和打滾。當前,sit() 和roll_over() 所做的有限,它們只是列印一條消息,指出小狗正蹲下或打滾。但可以擴展這些方法以模擬實際情況:如果這個類包含在一個電腦遊戲中,這些方法將包含創建小狗蹲下和打滾動畫效果的代碼。如果這個類是用於控制機器狗的,這些方法將引導機器狗做出蹲下和打滾的動作。
2. 在Python 2.7中創建類
在Python 2.7中創建類時,需要做細微的修改——在括弧內包含單詞object :
class
ClassName(object):
--snip--
這讓Python 2.7類的行為更像Python 3類,從而簡化了你的工作。
在Python 2.7中定義Dog 類時,代碼類似於下面這樣:
class
Dog(object):
--snip--
9.1.2 根據類創建實例
可將類視為有關如何創建實例的說明。Dog 類是一系列說明,讓Python知道如何創建表示特定小狗的實例。
下面來創建一個表示特定小狗的實例:
class Dog():
--snip--
❶ my_dog =
Dog('willie', 6)
❷
print("My dog's name is " + my_dog.name.title() + ".")
❸
print("My dog is " + str(my_dog.age) + " years old.")
這裡使用的是前一個示例中編寫的Dog 類。在❶處,我們讓Python創建一條名字為'willie' 、年齡為6 的小狗。遇到這行代碼時,Python使用實參'willie' 和6 調用Dog 類中的方法__init__() 。方法__init__() 創建一個表示特定小狗的示例,並使用我們提供的值來設置屬性name 和age 。方法__init__() 並未顯式地包含return 語句,但Python自動返回一個表示這條小狗的實例。我們將這個實例存儲在變數my_dog 中。在這裡,命名約定很有用:我們通常可以認為首字母大寫的名稱(如Dog )指的是類,而小寫的名稱(如my_dog )指的是根據類創建的實例。
1. 訪問屬性
要訪問實例的屬性,可使用句點標記法。在❷處,我們編寫了如下代碼來訪問my_dog 的屬性name 的值:
my_dog.name
句點標記法在Python中很常用,這種語法演示了Python如何獲悉屬性的值。在這裡,Python先找到實例my_dog ,再查找與這個實例相關聯的屬性name 。在Dog 類中引用這個屬性時,使用的是self.name 。在❸處,我們使用同樣的方法來獲取屬性age 的值。在前面的第1條print 語句中,my_dog.name.title() 將my_dog 的屬性name 的值'willie' 改為首字母大寫的;在第2條print 語句中,str(my_dog.age) 將my_dog 的屬性age 的值6 轉換為字串。
輸出是有關my_dog 的摘要:
My
dog's name is Willie.
My
dog is 6 years old.
2. 調用方法
根據Dog 類創建實例後,就可以使用句點標記法來調用Dog 類中定義的任何方法。下麵來讓小狗蹲下和打滾:
class
Dog():
--snip--
my_dog
= Dog('willie', 6)
my_dog.sit()
my_dog.roll_over()
要調用方法,可指定實例的名稱(這裡是my_dog )和要調用的方法,並用句點分隔它們。遇到代碼my_dog.sit() 時,Python在類Dog 中查找方法sit() 並運行其代碼。Python以同樣的方式解讀代碼my_dog.roll_over() 。
Willie按我們的命令做了:
Willie
is now sitting.
Willie
rolled over!
這種語法很有用。如果給屬性和方法指定了合適的描述性名稱,如name 、age 、sit() 和roll_over() ,即便是從未見過的代碼塊,我們也能夠輕鬆地推斷出它是做什麼的。
3. 創建多個實例
可按需求根據類創建任意數量的實例。下面再創建一個名為your_dog 的實例:
class
Dog():
--snip--
my_dog
= Dog('willie', 6)
your_dog
= Dog('lucy', 3)
print("My
dog's name is " + my_dog.name.title() + ".")
print("My
dog is " + str(my_dog.age) + " years old.")
my_dog.sit()
print("\nYour
dog's name is " + your_dog.name.title() + ".")
print("Your
dog is " + str(your_dog.age) + " years old.")
your_dog.sit()
在這個實例中,我們創建了兩條小狗,它們分別名為Willie和Lucy。每條小狗都是一個獨立的實例,有自己的一組屬性,能夠執行相同的操作:
My
dog's name is Willie.
My
dog is 6 years old.
Willie
is now sitting.
Your
dog's name is Lucy.
Your
dog is 3 years old.
Lucy
is now sitting.
就算我們給第二條小狗指定同樣的名字和年齡,Python依然會根據Dog 類創建另一個實例。你可按需求根據一個類創建任意數量的實例,條件是將每個實例都存儲在不同的變數中,或佔用清單或字典的不同位置。
動手試一試
9-1 餐館 :創建一個名為Restaurant 的類,其方法__init__() 設置兩個屬性:restaurant_name 和cuisine_type 。創建一個名為describe_restaurant() 的方法和一個名為open_restaurant() 的方法,其中前者列印前述兩項資訊,而後者列印一條消息,指出餐館正在營業。
根據這個類創建一個名為restaurant 的實例,分別列印其兩個屬性,再調用前述兩個方法。
9-2 三家餐館 :根據你為完成練習9-1而編寫的類創建三個實例,並對每個實例調用方法describe_restaurant() 。
9-3 用戶 :創建一個名為User 的類,其中包含屬性first_name 和last_name ,還有使用者簡介通常會存儲的其他幾個屬性。在類User 中定義一個名為describe_user() 的方法,它列印使用者資訊摘要;再定義一個名為greet_user() 的方法,它向用戶發出個性化的問候。
創建多個表示不同使用者的實例,並對每個實例都調用上述兩個方法。
9.2 使用類和實例
你可以使用類來模擬現實世界中的很多情景。類編寫好後,你的大部分時間都將花在使用根據類創建的實例上。你需要執行的一個重要任務是修改實例的屬性。你可以直接修改實例的屬性,也可以編寫方法以特定的方式進行修改。
9.2.1 Car 類
下面來編寫一個表示汽車的類,它存儲了有關汽車的資訊,還有一個匯總這些資訊的方法:
car.py
class Car():
"""一次模擬汽車的簡單嘗試"""
❶ def __init__(self, make, model, year):
"""初始化描述汽車的屬性"""
self.make = make
self.model = model
self.year = year
❷ def get_descriptive_name(self):
"""返回整潔的描述性資訊"""
long_name = str(self.year) + ' ' +
self.make + ' ' + self.model
return long_name.title()
❸
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
在❶處,我們定義了方法__init__() 。與前面的Dog 類中一樣,這個方法的第一個形參為self ;我們還在這個方法中包含了另外三個形參:make 、model 和year 。方法__init__() 接受這些形參的值,並將它們存儲在根據這個類創建的實例的屬性中。創建新的Car 實例時,我們需要指定其製造商、型號和生產年份。
在❷處,我們定義了一個名為get_descriptive_name() 的方法,它使用屬性year 、make 和model 創建一個對汽車進行描述的字串,讓我們無需分別列印每個屬性的值。為在這個方法中訪問屬性的值,我們使用了self.make 、self.model 和self.year 。在❸處,我們根據Car 類創建了一個實例,並將其存儲到變數my_new_car 中。接下來,我們調用方法get_descriptive_name() ,指出我們擁有的是一輛什麼樣的汽車:
2016
Audi A4
為讓這個類更有趣,下面給它添加一個隨時間變化的屬性,它存儲汽車的總里程。
9.2.2 給屬性指定預設值
類中的每個屬性都必須有初始值,哪怕這個值是0或空字串。在有些情況下,如設置預設值時,在方法__init__() 內指定這種初始值是可行的;如果你對某個屬性這樣做了,就無需包含為它提供初始值的形參。
下面來添加一個名為odometer_reading 的屬性,其初始值總是為0。我們還添加了一個名為read_odometer() 的方法,用於讀取汽車的里程表:
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):
--snip--
❷ def read_odometer(self):
"""列印一條指出汽車里程的消息"""
print("This car has " +
str(self.odometer_reading) + " miles on it.")
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
現在,當Python調用方法__init__() 來創建新實例時,將像前一個示例一樣以屬性的方式存儲製造商、型號和生產年份。接下來,Python將創建一個名為odometer_reading 的屬性,並將其初始值設置為0(見❶)。在❷處,我們還定義了一個名為read_odometer() 的方法,它讓你能夠輕鬆地獲悉汽車的里程。
一開始汽車的里程為0:
2016
Audi A4
This
car has 0 miles on it.
出售時里程表讀數為0的汽車並不多,因此我們需要一個修改該屬性的值的途徑。
9.2.3 修改屬性的值
可以以三種不同的方式修改屬性的值:直接通過實例進行修改;通過方法進行設置;通過方法進行遞增(增加特定的值)。下面依次介紹這些方法。
1. 直接修改屬性的值
要修改屬性的值,最簡單的方式是通過實例直接訪問它。下面的代碼直接將里程表讀數設置為23:
class Car():
--snip--
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()
在❶處,我們使用句點標記法來直接訪問並設置汽車的屬性odometer_reading 。這行代碼讓Python在實例my_new_car 中找到屬性odometer_reading ,並將該屬性的值設置為23:
2016
Audi A4
This
car has 23 miles on it.
有時候需要像這樣直接訪問屬性,但其他時候需要編寫對屬性進行更新的方法。
2. 通過方法修改屬性的值
如果有替你更新屬性的方法,將大有裨益。這樣,你就無需直接訪問屬性,而可將值傳遞給一個方法,由它在內部進行更新。
下面的示例演示了一個名為update_odometer() 的方法:
class Car():
--snip--
❶ def update_odometer(self, mileage):
"""將里程表讀數設置為指定的值"""
self.odometer_reading = mileage
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
❷
my_new_car.update_odometer(23)
my_new_car.read_odometer()
對Car 類所做的唯一修改是在❶處添加了方法update_odometer() 。這個方法接受一個里程值,並將其存儲到self.odometer_reading 中。在❷處,我們調用了update_odometer() ,並向它提供了實參23(該實參對應于方法定義中的形參mileage )。它將里程表讀數設置為23;而方法read_odometer() 列印該讀數:
2016
Audi A4
This
car has 23 miles on it.
可對方法update_odometer() 進行擴展,使其在修改里程表讀數時做些額外的工作。下面來添加一些邏輯,禁止任何人將里程表讀數往回檔:
class Car():
--snip--
def update_odometer(self, mileage):
"""
將里程表讀數設置為指定的值
禁止將里程表讀數往回檔
"""
❶ if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
❷ print("You can't roll back an
odometer!")
現在,update_odometer() 在修改屬性前檢查指定的讀數是否合理。如果新指定的里程(mileage )大於或等於原來的里程(self.odometer_reading ),就將里程表讀數改為新指定的里程(見❶);否則就發出警告,指出不能將里程表往回撥(見❷)。
3. 通過方法對屬性的值進行遞增
有時候需要將屬性值遞增特定的量,而不是將其設置為全新的值。假設我們購買了一輛二手車,且從購買到登記期間增加了100英里的里程,下面的方法讓我們能夠傳遞這個增量,並相應地增加里程表讀數:
class Car():
--snip--
def update_odometer(self, mileage):
--snip--
❶ def increment_odometer(self, miles):
"""將里程表讀數增加指定的量"""
self.odometer_reading += miles
❷
my_used_car = Car('subaru', 'outback', 2013)
print(my_used_car.get_descriptive_name())
❸
my_used_car.update_odometer(23500)
my_used_car.read_odometer()
❹
my_used_car.increment_odometer(100)
my_used_car.read_odometer()
在❶處,新增的方法increment_odometer() 接受一個單位為英里的數位,並將其加入到self.odometer_reading 中。在❷處,我們創建了一輛二手車——my_used_car 。在❸處,我們調用方法update_odometer() 並傳入23500 ,將這輛二手車的里程表讀數設置為23 500。在❹處,我們調用increment_odometer() 並傳入100 ,以增加從購買到登記期間行駛的100英里:
2013
Subaru Outback
This
car has 23500 miles on it.
This
car has 23600 miles on it.
你可以輕鬆地修改這個方法,以禁止增量為負值,從而防止有人利用它來回撥里程表。
注意 你可以使用類似於上面的方法來控制使用者修改屬性值(如里程表讀數)的方式,但能夠訪問程式的人都可以通過直接訪問屬性來將里程表修改為任何值。要確保安全,除了進行類似於前面的基本檢查外,還需特別注意細節。
動手試一試
9-4 就餐人數 :在為完成練習9-1而編寫的程式中,添加一個名為number_served 的屬性,並將其預設值設置為0。根據這個類創建一個名為restaurant 的實例;列印有多少人在這家餐館就餐過,然後修改這個值並再次列印它。
添加一個名為set_number_served() 的方法,它讓你能夠設置就餐人數。調用這個方法並向它傳遞一個值,然後再次列印這個值。
添加一個名為increment_number_served() 的方法,它讓你能夠將就餐人數遞增。調用這個方法並向它傳遞一個這樣的值:你認為這家餐館每天可能接待的就餐人數。
9-5 嘗試登錄次數 :在為完成練習9-3而編寫的User 類中,添加一個名為login_attempts 的屬性。編寫一個名為increment_login_attempts() 的方法,它將屬性login_attempts 的值加1。再編寫一個名為reset_login_attempts() 的方法,它將屬性login_attempts 的值重置為0。
根據User 類創建一個實例,再調用方法increment_login_attempts() 多次。列印屬性login_attempts 的值,確認它被正確地遞增;然後,調用方法reset_login_attempts() ,並再次列印屬性login_attempts 的值,確認它被重置為0。
9.3 繼承
編寫類時,並非總是要從空白開始。如果你要編寫的類是另一個現成類的特殊版本,可使用繼承 。一個類繼承 另一個類時,它將自動獲得另一個類的所有屬性和方法;原有的類稱為父類 ,而新類稱為子類 。子類繼承了其父類的所有屬性和方法,同時還可以定義自己的屬性和方法。
9.3.1 子類的方法__init__()
創建子類的實例時,Python首先需要完成的任務是給父類的所有屬性賦值。為此,子類的方法__init__() 需要父類施以援手。
例如,下面來模擬電動汽車。電動汽車是一種特殊的汽車,因此我們可以在前面創建的Car 類的基礎上創建新類ElectricCar ,這樣我們就只需為電動汽車特有的屬性和行為編寫代碼。
下面來創建一個簡單的ElectricCar 類版本,它具備Car 類的所有功能:
electric_car.py
❶ 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):
"""電動汽車的獨特之處"""
❸ def __init__(self, make, model, year):
"""初始化父類的屬性"""
❹ super().__init__(make, model, year)
❺ my_tesla
= ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
首先是Car 類的代碼(見❶)。創建子類時,父類必須包含在當前檔中,且位於子類前面。在❷處,我們定義了子類ElectricCar 。定義子類時,必須在括弧內指定父類的名稱。方法__init__() 接受創建Car 實例所需的資訊(見❸)。
❹處的super() 是一個特殊函數,説明Python將父類和子類關聯起來。這行代碼讓Python調用ElectricCar 的父類的方法__init__() ,讓ElectricCar 實例包含父類的所有屬性。父類也稱為超類 (superclass),名稱super因此而得名。
為測試繼承是否能夠正確地發揮作用,我們嘗試創建一輛電動汽車,但提供的資訊與創建普通汽車時相同。在❺處,我們創建ElectricCar 類的一個實例,並將其存儲在變數my_tesla 中。這行代碼調用ElectricCar 類中定義的方法__init__() ,後者讓Python調用父類Car 中定義的方法__init__() 。我們提供了實參'tesla' 、'model s' 和2016 。
除方法__init__() 外,電動汽車沒有其他特有的屬性和方法。當前,我們只想確認電動汽車具備普通汽車的行為:
2016
Tesla Model S
ElectricCar 實例的行為與Car 實例一樣,現在可以開始定義電動汽車特有的屬性和方法了。
9.3.2 Python 2.7中的繼承
在Python 2.7中,繼承語法稍有不同,ElectricCar 類的定義類似於下面這樣:
class
Car(object):
def __init__(self, make, model, year):
--snip--
class
ElectricCar(Car):
def __init__(self, make, model, year):
super(ElectricCar, self).__init__(make,
model, year)
--snip--
函數super() 需要兩個實參:子類名和對象self 。為幫助Python將父類和子類關聯起來,這些實參必不可少。另外,在Python 2.7中使用繼承時,務必在定義父類時在括弧內指定object 。
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()
在❶處,我們添加了新屬性self.battery_size ,並設置其初始值(如70 )。根據ElectricCar 類創建的所有實例都將包含這個屬性,但所有Car 實例都不包含它。在❷處,我們還添加了一個名為describe_battery() 的方法,它列印有關電瓶的資訊。我們調用這個方法時,將看到一條電動汽車特有的描述:
2016
Tesla Model S
This
car has a 70-kWh battery.
對於ElectricCar 類的特殊化程度沒有任何限制。類比電動汽車時,你可以根據所需的準確程度添加任意數量的屬性和方法。如果一個屬性或方法是任何汽車都有的,而不是電動汽車特有的,就應將其加入到Car 類而不是ElectricCar 類中。這樣,使用Car 類的人將獲得相應的功能,而ElectricCar 類只包含處理電動汽車特有屬性和行為的代碼。
9.3.4 重寫父類的方法
對於父類的方法,只要它不符合子類模擬的實物的行為,都可對其進行重寫。為此,可在子類中定義一個這樣的方法,即它與要重寫的父類方法同名。這樣,Python將不會考慮這個父類方法,而只關注你在子類中定義的相應方法。
假設Car 類有一個名為fill_gas_tank() 的方法,它對全電動汽車來說毫無意義,因此你可能想重寫它。下面演示了一種重寫方式:
def
ElectricCar(Car):
--snip--
def fill_gas_tank():
"""電動汽車沒有油箱"""
print("This car doesn't need a gas
tank!")
現在,如果有人對電動汽車調用方法fill_gas_tank() ,Python將忽略Car 類中的方法fill_gas_tank() ,轉而運行上述代碼。使用繼承時,可讓子類保留從父類那裡繼承而來的精華,並剔除不需要的糟粕。
9.3.5 將實例用作屬性
使用代碼模擬實物時,你可能會發現自己給類添加的細節越來越多:屬性和方法清單以及檔都越來越長。在這種情況下,可能需要將類的一部分作為一個獨立的類提取出來。你可以將大型類拆分成多個協同工作的小類。
例如,不斷給ElectricCar 類添加細節時,我們可能會發現其中包含很多專門針對汽車電瓶的屬性和方法。在這種情況下,我們可將這些屬性和方法提取出來,放到另一個名為Battery 的類中,並將一個Battery 實例用作ElectricCar 類的一個屬性:
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()
在❶處,我們定義了一個名為Battery 的新類,它沒有繼承任何類。❷處的方法__init__() 除self 外,還有另一個形參battery_size 。這個形參是可選的:如果沒有給它提供值,電瓶容量將被設置為70。方法describe_battery() 也移到了這個類中(見❸)。
在ElectricCar 類中,我們添加了一個名為self.battery 的屬性(見❹)。這行代碼讓Python創建一個新的Battery 實例(由於沒有指定尺寸,因此為預設值70 ),並將該實例存儲在屬性self.battery 中。每當方法__init__() 被調用時,都將執行該操作;因此現在每個ElectricCar 實例都包含一個自動創建的Battery 實例。
我們創建一輛電動汽車,並將其存儲在變數my_tesla 中。要描述電瓶時,需要使用電動汽車的屬性battery :
my_tesla.battery.describe_battery()
這行代碼讓Python在實例my_tesla 中查找屬性battery ,並對存儲在該屬性中的Battery 實例調用方法describe_battery() 。
輸出與我們前面看到的相同:
2016
Tesla Model S
This
car has a 70-kWh battery.
這看似做了很多額外的工作,但現在我們想多詳細地描述電瓶都可以,且不會導致ElectricCar 類混亂不堪。下面再給Battery 類添加一個方法,它根據電瓶容量報告汽車的續航里程:
class Car():
--snip--
class Battery():
--snip--
❶ 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):
--snip--
my_tesla = ElectricCar('tesla', 'model s',
2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
❷ my_tesla.battery.get_range()
❶處新增的方法get_range() 做了一些簡單的分析:如果電瓶的容量為70kWh,它就將續航里程設置為240英里;如果容量為85kWh,就將續航里程設置為270英里,然後報告這個值。為使用這個方法,我們也通過汽車的屬性battery 來調用它(見❷)。
輸出指出了汽車的續航里程(這取決於電瓶的容量):
2016
Tesla Model S
This
car has a 70-kWh battery.
This
car can go approximately 240 miles on a full charge.
9.3.6 模擬實物
模擬較複雜的物件(如電動汽車)時,需要解決一些有趣的問題。續航里程是電瓶的屬性還是汽車的屬性呢?如果我們只需描述一輛汽車,那麼將方法get_range() 放在Battery 類中也許是合適的;但如果要描述一家汽車製造商的整個產品線,也許應該將方法get_range() 移到ElectricCar 類中。在這種情況下,get_range() 依然根據電瓶容量來確定續航里程,但報告的是一款汽車的續航里程。我們也可以這樣做:將方法get_range() 還留在Battery 類中,但向它傳遞一個參數,如car_model ;在這種情況下,方法get_range() 將根據電瓶容量和汽車型號報告續航里程。
這讓你進入了程式師的另一個境界:解決上述問題時,你從較高的邏輯層面(而不是語法層面)考慮;你考慮的不是Python,而是如何使用代碼來表示實物。到達這種境界後,你經常會發現,現實世界的建模方法並沒有對錯之分。有些方法的效率更高,但要找出效率最高的標記法,需要經過一定的實踐。只要代碼像你希望的那樣運行,就說明你做得很好!即便你發現自己不得不多次嘗試使用不同的方法來重寫類,也不必氣餒;要編寫出高效、準確的代碼,都得經過這樣的過程。
動手試一試
9-6 霜淇淋小店 :霜淇淋小店是一種特殊的餐館。編寫一個名為IceCreamStand 的類,讓它繼承你為完成練習9-1或練習9-4而編寫的Restaurant 類。這兩個版本的Restaurant 類都可以,挑選你更喜歡的那個即可。添加一個名為flavors 的屬性,用於存儲一個由各種口味的霜淇淋組成的列表。編寫一個顯示這些霜淇淋的方法。創建一個IceCreamStand 實例,並調用這個方法。
9-7 管理員 :管理員是一種特殊的用戶。編寫一個名為Admin 的類,讓它繼承你為完成練習9-3或練習9-5而編寫的User 類。添加一個名為privileges 的屬性,用於存儲一個由字串(如"can add post" 、"can delete post" 、"can ban user" 等)組成的列表。編寫一個名為show_privileges() 的方法,它顯示管理員的許可權。創建一個Admin 實例,並調用這個方法。
9-8 許可權 :編寫一個名為Privileges 的類,它只有一個屬性——privileges ,其中存儲了練習9-7 所說的字串清單。將方法show_privileges() 移到這個類中。在Admin 類中,將一個Privileges 實例用作其屬性。創建一個Admin 實例,並使用方法show_privileges() 來顯示其許可權。
9-9 電瓶升級 :在本節最後一個electric_car.py版本中,給Battery 類添加一個名為upgrade_battery() 的方法。這個方法檢查電瓶容量,如果它不是85,就將它設置為85。創建一輛電瓶容量為預設值的電動汽車,調用方法get_range() ,然後對電瓶進行升級,並再次調用get_range() 。你會看到這輛汽車的續航里程增加了。
9.4 導入類
隨著你不斷地給類添加功能,檔可能變得很長,即便你妥善地使用了繼承亦如此。為遵循Python的總體理念,應讓文件盡可能整潔。為在這方面提供幫助,Python允許你將類存儲在模組中,然後在主程序中導入所需的模組。
9.4.1 導入單個類
下面來創建一個隻包含Car 類的模組。這讓我們面臨一個微妙的命名問題:在本章中,已經有一個名為car.py的檔,但這個模組也應命名為car.py,因為它包含表示汽車的代碼。我們將這樣解決這個命名問題:將Car 類存儲在一個名為car.py的模組中,該模組將覆蓋前面使用的檔car.py。從現在開始,使用該模組的程式都必須使用更具體的檔案名,如my_car.py。下麵是模組car.py,其中只包含Car 類的代碼:
car.py
❶
"""一個可用於表示汽車的類"""
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,在其中導入Car 類並創建其實例:
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()
❶處的import 語句讓Python打開模組car ,並導入其中的Car 類。這樣我們就可以使用Car 類了,就像它是在這個檔中定義的一樣。輸出與我們在前面看到的一樣:
2016
Audi A4
This
car has 23 miles on it.
導入類是一種有效的程式設計方式。如果在這個程式中包含了整個Car 類,它該有多長呀!通過將這個類移到一個模組中,並導入該模組,你依然可以使用其所有功能,但主程序檔變得整潔而易於閱讀了。這還能讓你將大部分邏輯存儲在獨立的檔中;確定類像你希望的那樣工作後,你就可以不管這些檔,而專注於主程序的高級邏輯了。
9.4.2 在一個模組中存儲多個類
雖然同一個模組中的類之間應存在某種相關性,但可根據需要在一個模組中存儲任意數量的類。類Battery 和ElectricCar 都可幫助模擬汽車,因此下面將它們都加入模組car.py中:
car.py
"""一組用於表示燃油汽車和電動汽車的類"""
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()
輸出與我們前面看到的相同,但大部分邏輯都隱藏在一個模組中:
2016
Tesla Model S
This
car has a 70-kWh battery.
This
car can go approximately 240 miles on a full charge.
9.4.3 從一個模組中導入多個類
可根據需要在程式檔中導入任意數量的類。如果我們要在同一個程式中創建普通汽車和電動汽車,就需要將Car 和ElectricCar 類都導入:
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())
在❶處從一個模組中導入多個類時,用逗號分隔了各個類。導入必要的類後,就可根據需要創建每個類的任意數量的實例。
在這個示例中,我們在❷處創建了一輛大眾甲殼蟲普通汽車,並在❸處創建了一輛特斯拉Roadster電動汽車:
2016
Volkswagen Beetle
2016
Tesla Roadster
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())
在❶處,我們導入了整個car 模組。接下來,我們使用語法 module_name.class_name 訪問需要的類。像前面一樣,我們在❷處創建了一輛大眾甲殼蟲汽車,並在❸處創建了一輛特斯拉Roadster汽車。
9.4.5 導入模組中的所有類
要導入模組中的每個類,可使用下面的語法:
from
module_name import *
不推薦使用這種導入方式,其原因有二。首先,如果只要看一下檔開頭的import 語句,就能清楚地知道程式使用了哪些類,將大有裨益;但這種導入方式沒有明確地指出你使用了模組中的哪些類。這種導入方式還可能引發名稱方面的困惑。如果你不小心導入了一個與程式檔中其他東西同名的類,將引發難以診斷的錯誤。這裡之所以介紹這種導入方式,是因為雖然不推薦使用這種方式,但你可能會在別人編寫的代碼中見到它。
需要從一個模組中導入很多類時,最好導入整個模組,並使用 module_name.class_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())
在❶處,我們從模組car 中導入了Car 類,並從模組electric_car 中導入ElectricCar 類。接下來,我們創建了一輛普通汽車和一輛電動汽車。這兩種汽車都得以正確地創建:
2016
Volkswagen Beetle
2016
Tesla Roadster
9.4.7 自訂工作流程
正如你看到的,在組織大型專案的代碼方面,Python提供了很多選項。熟悉所有這些選項很重要,這樣你才能確定哪種專案組織方式是最佳的,並能理解別人開發的專案。
一開始應讓代碼結構盡可能簡單。先盡可能在一個檔中完成所有的工作,確定一切都能正確運行後,再將類移到獨立的模組中。如果你喜歡模組和檔的對話模式,可在專案開始時就嘗試將類存儲到模組中。先找出讓你能夠編寫出可行代碼的方式,再嘗試讓代碼更為組織有序。
動手試一試
9-10 導入Restaurant 類 :將最新的Restaurant 類存儲在一個模組中。在另一個檔中,導入Restaurant 類,創建一個Restaurant 實例,並調用Restaurant 的一個方法,以確認import 語句正確無誤。
9-11 導入Admin 類 :以為完成練習9-8而做的工作為基礎,將User 、Privileges 和Admin 類存儲在一個模組中,再創建一個檔,在其中創建一個Admin 實例並對其調用方法show_privileges() ,以確認一切都能正確地運行。
9-12 多個模組 :將User 類存儲在一個模組中,並將Privileges 和Admin 類存儲在另一個模組中。再創建一個檔,在其中創建一個Admin 實例,並對其調用方法show_privileges() ,以確認一切都依然能夠正確地運行。
9.5 Python標準庫
Python標準庫 是一組模組,安裝的Python都包含它。你現在對類的工作原理已有大致的瞭解,可以開始使用其他程式師編寫好的模組了。可使用標準庫中的任何函數和類,為此只需在程式開頭包含一條簡單的import 語句。下面來看模組collections 中的一個類——OrderedDict 。
字典讓你能夠將資訊關聯起來,但它們不記錄你添加鍵—值對的順序。要創建字典並記錄其中的鍵—值對的添加順序,可使用模組collections 中的OrderedDict 類。OrderedDict 實例的行為幾乎與字典相同,區別只在於記錄了鍵—值對的添加順序。
我們再來看一看第6章的favorite_languages.py示例,但這次將記錄被調查者參與調查的順序:
favorite_languages.py
❶ from
collections import OrderedDict
❷
favorite_languages = OrderedDict()
❸
favorite_languages['jen'] = 'python'
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'python'
❹ for name,
language in favorite_languages.items():
print(name.title() + "'s favorite
language is " +
language.title() + ".")
我們首先從模組collections 中導入了OrderedDict 類(見❶)。在❷處,我們創建了OrderedDict 類的一個實例,並將其存儲到favorite_languages 中。請注意,這裡沒有使用花括弧,而是調用OrderedDict() 來創建一個空的有序字典,並將其存儲在favorite_languages 中。接下來,我們以每次一對的方式添加名字—語言對(見❸)。在❹處,我們遍歷favorite_languages ,但知道將以添加的順序獲取調查結果:
Jen's
favorite language is Python.
Sarah's
favorite language is C.
Edward's
favorite language is Ruby.
Phil's
favorite language is Python.
這是一個很不錯的類,它兼具清單和字典的主要優點(在將資訊關聯起來的同時保留原來的順序)。等你開始對關心的現實情形建模時,可能會發現有序字典正好能夠滿足需求。隨著你對標準庫的瞭解越來越深入,將熟悉大量可幫助你處理常見情形的模組。
注意 你還可以從其他地方下載外部模組。本書第二部分的每個專案都需要使用外部模組,屆時你將看到很多這樣的示例。
動手試一試
9-13 使用OrderedDict :在練習6-4中,你使用了一個標準字典來表示詞彙表。請使用OrderedDict 類來重寫這個程式,並確認輸出的順序與你在字典中添加鍵—值對的順序一致。
9-14 骰子 :模組random 包含以各種方式生成亂數的函數,其中的randint() 返回一個位於指定範圍內的整數,例如,下面的代碼返回一個1~6內的整數:
from
random import randint
x =
randint(1, 6)
請創建一個Die 類,它包含一個名為sides 的屬性,該屬性的預設值為6。編寫一個名為roll_die() 的方法,它列印位於1和骰子面數之間的亂數。創建一個6面的骰子,再擲10次。 創建一個10面的骰子和一個20面的骰子,並將它們都擲10次。
9-15 Python Module of the Week :要瞭解Python標準庫,一個很不錯的資源是網站Python Module of the Week。請訪問http://pymotw.com/ 並查看其中的目錄,在其中找一個你感興趣的模組進行探索,或閱讀模組collections 和random 的文檔。
9.6 類編碼風格
你必須熟悉有些與類相關的編碼風格問題,在你編寫的程式較複雜時尤其如此。
類名應採用駝峰命名法 ,即將類名中的每個單詞的首字母都大寫,而不使用底線。實例名和模組名都採用小寫格式,並在單詞之間加上底線。
對於每個類,都應緊跟在類定義後面包含一個文檔字串。這種文檔字串簡要地描述類的功能,並遵循編寫函數的文檔字串時採用的格式約定。每個模組也都應包含一個文檔字串,對其中的類可用於做什麼進行描述。
可使用空行來組織代碼,但不要濫用。在類中,可使用一個空行來分隔方法;而在模組中,可使用兩個空行來分隔類。
需要同時導入標準庫中的模組和你編寫的模組時,先編寫導入標準庫模組的import 語句,再添加一個空行,然後編寫導入你自己編寫的模組的import 語句。在包含多條import 語句的程式中,這種做法讓人更容易明白程式使用的各個模組都來自何方。
9.7 小結
在本章中,你學習了:如何編寫類;如何使用屬性在類中存儲資訊,以及如何編寫方法,以讓類具備所需的行為;如何編寫方法__init__() ,以便根據類創建包含所需屬性的實例。你見識了如何修改實例的屬性——包括直接修改以及通過方法進行修改。你還瞭解了:使用繼承可簡化相關類的創建工作;將一個類的實例用作另一個類的屬性可讓類更簡潔。
你瞭解到,通過將類存儲在模組中,並在需要使用這些類的檔中導入它們,可讓專案組織有序。你學習了Python標準庫,並見識了一個使用模組collections 中的OrderedDict 類的示例。最後,你學習了編寫類時應遵循的Python約定。
在第10章中,你將學習如何使用檔,這讓你能夠保存你在程式中所做的工作,以及你讓用戶做的工作。你還將學習異常 ,這是一種特殊的Python類,用於説明你在發生錯誤時採取相應的措施。

0 留言:
發佈留言