2020年10月29日星期四

014 與小卡特一起學 Python 第 14 章 對象

 申明本站飛宇網 https://feiyetopro.blogspot.com/自網路收集整理之書籍文章影音僅供預覽交流學習研究,其[書籍、文章、影音]情節內容, 評論屬其個人行為, 與本網站無關。版權歸原作者和出版社所有,請在下載 24 小時內刪除,不得用作商業用途;如果您喜歡其作品,請支持訂閱購買[正版]謝謝!


14 章 對象

在前幾章中,我們已經瞭解了可以使用不同方式組織資料和程式,以及把東西收集在一起。我們看到了清單可以收集變數(資料),函數可以把一些代碼收集到能夠反復使用的單元中。


物件(object)則讓這種收集的思想更向前邁進一步。物件可以把函數和資料收集在一起。這個主意在程式設計中非常有用,而且在很多很多的程式中都已經用到。實際上,如果仔細分析 Python,幾乎一切都是物件。按程式設計的術語來講,我們說 Python 是物件導向的(object oriented)。這說明,Python 中可以使用物件(實際上這也相當容易)。並不是一定得創建自己的物件,不過這樣可以讓很多事情更容易一些。

在這一章中,我們將學習什麼是物件,以及如何創建和使用物件。後面幾章開始處理圖形時,我們將會大量使用物件。

 

14.1 真實世界中的物件

什麼是物件?如果我們不是在討論程式設計,當我問到這個問題時,可能會有下面的對話:


Python 中定義什麼是物件也可以作為一個很好的起點。拿球來舉個例子。可以操作一個球,比如撿球、拋球、踢球或者充氣(對於某些球來說)。我們把這些操作稱為動作(action)。還可以通過指出球的顏色、大小和重量來描述一個球。這些就是球的屬性(attribute)。

術語箱

可以通過描述特徵或屬性來描述一個物件。球的屬性之一是它的形狀。大多數球都是圓形。還有一些其他的屬性,比如顏色、大小、重量和價格。屬性的另一個說法是特性(property)。

真實世界的真實物件(物體)包括兩個方面。

·    可以對它們做什麼(動作)。

·    如何描述(屬性或特性)。

程式設計中也是如此。

 

14.2 Python 中的物件

Python 中,一個物件的特徵(或你知道的事情)也稱為屬性(attribute),這應該很好記。動作(或能夠對物件做的操作)稱為方法(method)。

如果要建立一個球的 Python 版本或者模型(model),球就是一個物件,它要有屬性和方法。

球的屬性可能包括:

ball.color

ball.size

ball.weight

 

這些都是關於球的描述。

球的方法可能包括:

ball.kick()

ball.throw()

ball.inflate()

 

這些都是可以對球做的操作。

什麼是屬性

屬性就是你所知道(或者可以得出)的關於球的所有方面。球的屬性就是一些資訊(數位、字串等等)。聽起來很熟悉?沒錯,它們就是變數,只不過是包含在物件中的變數。

可以顯示:

print ball.size

 

可以為它們賦值:

ball.color = 'green'

 

可以把它們賦給常規的、不是物件的變數:

myColor = ball.color

 

還可以把它們賦給其他物件的屬性:

myBall.color = yourBall.color

 

什麼是方法

方法就是可以對物件做的操作,它們是一些代碼塊,可以調用這些代碼塊來完成某個工作。聽起來很熟悉?沒錯,方法就是包含在物件中的函數。

函數能做到的,方法都可以做到,包括傳遞參數和返回值。

 

14.3 物件 = 屬性 + 方法

所以利用物件,可以把一個東西的屬性和方法(你知道的事情和你可以做的事情)收集在一起。屬性是資訊,方法是動作。

 

14.4 這個點是什麼

在前面的球例子中,你可能已經注意到物件名與屬性或方法名之間的點。這是 Python 使用物件屬性和方法的一種記法:object.attribute  object.method()。就這麼簡單。這稱為點記法,很多程式設計語言中都使用了這種記法。

現在對於物件已經有了整體認識。下面來建立一些物件!

 

14.5 創建對象


Python 中創建對象包括兩步。

第一步是定義物件看上去什麼樣,會做什麼,也就是它的屬性和方法。但是創建這個描述並不會真正創建一個物件。這有點像一個房子的藍圖。藍圖可以告訴你房子看上去怎麼樣,但是藍圖本身並不是一個房子。你不可能住在一個藍圖裡。只能用它來建造真正的房子。實際上,可以使用藍圖蓋很多的房子。

Python 中,物件的描述或藍圖稱為一個類(class)。

第二步是使用類來建立一個真正的物件。這個物件稱為這個類的一個實例(instance)。

下面來看一個建立類和實例的例子。代碼清單 14-1 顯示了一個簡單的 Ball 類的類定義。


代碼清單
14-1 創建一個簡單的 Ball 

代碼清單 14-1 是一個球的類定義,其中只有一個方法 bounce()。不過,屬性呢?嗯,屬性並不屬於類,它們屬於各個實例。因為每個實例可以有不同的屬性。

設置實例屬性有兩種方法。後面的小節中我們會分別瞭解這兩種方法。

創建一個物件實例

前面提到過,類定義並不是一個物件。這只是藍圖。現在來蓋真正的房子。

如果想創建 Ball 的一個實例,可以這樣做:

myBall = Ball()

 

這個球還沒有任何屬性,所以下面給它提供一些屬性:

myBall.direction = "down"

myBall.color = "green"

myBall.size = "small"

 

這是為物件定義屬性的一種方法。下一節還會學習另一種方法。

現在來試試它的方法。我們要這樣使用 bounce() 方法:

myBall.bounce()

 

下面把這些都放在一個程式裡,增加一些 print 語句來看發生了什麼。程式見代碼清單 14-2

代碼清單 14-2 使用 Ball 


運行這個程式,可以看到下面的結果:


注意,調用 bounce() 方法會把球的方向(direction)從下(down)改為上(up),這正是 bounce() 方法中的代碼所要做的。

初始化對象

創建球物件時,並沒有在 sizecolor  direction 中填入任何內容。必須在創建物件之後填充這些內容。不過有一種方法可以在創建物件時設置屬性。這稱為初始化對象。

術語箱

初始化(initializing)表示開始時做好準備。在軟體中對某個東西初始化時,就是把它設置成一種我們希望的狀態或條件,以備使用。

創建類定義時,可以定義一個特定的方法,名為 __init__(),只要創建這個類的一個新實例,就會運行這個方法。可以向 __init__() 方法傳遞參數,這樣創建實例時就會把屬性設置為你希望的值。代碼清單 14-3 顯示了這是如何實現的。

代碼清單 14-3 增加一個 __init__() 方法


如果這個程式,得到的輸出應該與代碼清單 14-2 的相同。區別在於,代碼清單 14-3 使用了 __init__()方法來設置屬性。


謝謝你的提醒,卡特。在下一節中,我們將會瞭解這些魔法方法到底是什麼。

魔法方法 : str()

就像卡特說的,Python 中的物件有一些魔法方法,當然它們並不是真的有魔法!這些只是在你創建類時 Python 自動包含的一些方法。Python 程式師通常把它們叫做特殊方法(special method)。


我們已經知道, __init__() 方法會在物件創建時完成初始化。每個物件都內置有一個 __init__() 方法。如果你在類定義中沒有加入自己的 __init__() 方法,就會有這樣一個內置方法接管,它的工作就是創建物件。

另一個特殊方法是 __str__(),它會告訴 Python 列印(print)一個物件時具體顯示什麼內容。Python 會預設以下內容。

·    實例在哪裡定義(卡特的例子中,就是在 __main__中,這是程式的主部分)。

·    類名(Ball)。

·    存儲實例的記憶體位置(0x00BB83A0 部分)。

不過,如果你希望 print 為物件顯示其他的內容,可以定義自己的 __str__(),這會覆蓋內置的 __str__() 方法。代碼清單 14-4 舉了個例子。

代碼清單 14-4 使用 __str__() 改變列印物件的方式


現在運行這個程式,可以得到下面的結果:

>>> ================= RESTART =================

>>> 

Hi, I'm a small red ball!

 

這看起來比 <__main__.Ball instance at 0x00BB83A0> 好多了,你認為呢?所有魔法方法都在方法名稱前後各使用兩條底線。

什麼是 self

你可能已經注意到,在類屬性和方法定義中多處出現了“self”,比如:

def bounce(self):

 

self 是什麼意思?嗯,我們說過,可以使用藍圖蓋很多個房子,還記得吧?使用一個類也可以創建多個物件實例,例如:


調用其中一個實例的方法時,像這樣:

warrensBall.bounce()

 

方法必須知道是哪個實例調用了它。是 cartersBall 需要反彈嗎?還是 warrens Ball self 參數會告訴方法哪個物件調用它。這稱為實例引用(instance reference)。

不過先等等!調用方法時,warrensBall.bounce() 的括弧裡沒有參數,但是方法裡卻有一個 self 參數。既然我們並沒有傳入任何東西,這個 self 參數從哪裡來的?這是 Python 處理物件的另外一個魔法。調用一個類方法時,究竟是哪個實例調用了這個方法?這個資訊(也就是實例引用)會自動傳遞給方法。

這就像寫成:

Ball.bounce(warrensBall)

 

在這種情況下,我們告訴了 bounce() 方法哪個球要反彈。實際上,這個代碼也能正常工作,因為寫成 warrensBall.bounce() 時,Python 在後臺確實也是這麼做的。

順便說一句,self 這個名字在 Python 中沒有任何特殊的含義。只不過所有人都使用這個實例引用名。這也是讓代碼更易讀的一個約定。也可以把這個執行個體變數命名為你想要的任何名字,不過強烈建議你遵循這個約定,因為使用 self 能減少混亂。

我們在第 11 章建立了一個熱狗程式。現在作為使用物件的例子,我們來為熱狗建立一個類。

 

14.6 一個示例類—— HotDog

在這個例子中,我們假設熱狗總包括一個小麵包。(否則可真是一團糟。)下面為熱狗指定一些屬性和方法。

下麵是熱狗的屬性。

·    cooked_level:這是一個數位,通過這個屬性我們可以知道熱狗烤了多長時間。0 3 表示還是生的,超過 3 表示半生不熟,超過 5 表示已經烤好,超過 8 表示已經烤成木炭了!我們的熱狗開始時是生的。

·    cooked_string:這是一個字串,描述熱狗的生熟程度。

·    condiments:這是熱狗上的配料列表,比如番茄醬、芥末醬等。

下面是熱狗的方法。

·    cook():把熱狗烤一段時間。這會讓熱狗越來越熟。

·    add_condiment():給熱狗加一些配料。

·    __init__():創建實例並設置預設屬性。

·    __str__():讓 print 的結果看起來更好一些。

首先,需要定義類。先定義 __init__() 方法,它會為熱狗設置預設屬性:

class HotDog:

    def __init__(self):

        self.cooked_level = 0

        self.cooked_string = "Raw"

        self.condiments = []

 

先從一個沒有加任何配料的生熱狗開始。


現在,來建立一個方法烤熱狗:


繼續下面的工作之前,先對這一部分做個測試。首先,需要創建熱狗的一個實例,還要檢查它的屬性。

myDog = HotDog()

print myDog.cooked_level

print myDog.cooked_string

print myDog.condiments

 

下面把這些內容都放在一個程式中,運行這個程式。代碼清單 14-5 顯示了(到目前為止)完整的程式。

代碼清單 14-5 熱狗程式的開始部分

class HotDog:

    def __init__(self):

        self.cooked_level = 0

        self.cooked_string = "Raw"

        self.condiments = []

    def cook(self, time):

        self.cooked_level = self.cooked_level + time

        if self.cooked_level > 8:

            self.cooked_string = "Charcoal"

        elif self.cooked_level > 5:

            self.cooked_string = "Well-done"

        elif self.cooked_level > 3:

            self.cooked_string = "Medium"

        else:

            self.cooked_string = "Raw"

myDog = HotDog()

print myDog.cooked_level

print myDog.cooked_string

print myDog.condiments

 

像(Python)程式師一樣思考

Python 中的另一個約定是類名總是以大寫字母開頭。目前為止,我們已經見到 Ball  HotDog,所以說我們一直都在遵循這個約定。

現在,運行代碼清單 14-5 中的代碼,看看會得到什麼。結果應該像這樣:


可以看到,屬性分別是 cooked_level = 0cooked_string = "Raw",另外 condiments 為空。

現在來測試 cook() 方法。把下面的代碼行增加到代碼清單 14-5 中:


再運行這個程式,現在輸出會變成:


看來我們的 cook() 方法能正常工作。cooked_level  0 變成 4,而且字串也得到更新(從 Raw 變成 Medium)。

下面來增加一些配料。這需要一個新的方法。另外還可以自己增加 __str__()函數,讓列印物件更為容易。按代碼清單 14-6 編輯程式。

代碼清單 14-6 包含 cook()add_condiments()  __str__()  HotDog 


這個代碼清單有點兒長,但我還是建議你自己鍵入這些代碼,而且你已經有了之前代碼清單 14-5 中的部分代碼,不過,如果你的手指確實很累,或者你沒有時間,也可以在 \examples 資料夾或本書網站上找到這個代碼。

運行這個程式,看看能得到什麼。結果應該如下:

>>> ================================ RESTART ================================

>>> 

Raw hot dog.

Cooking hot dog for 4 minutes...

Medium hot dog.

Cooking hot dog for 3 more minutes...

Well-done hot dog.

What happens if I cook it for 10 more minutes?

Charcoal hot dog.

Now, I'm going to add some stuff on my hot dog

Charcoal hot dog with ketchup, mustard.

 

程式的第一部分創建了類。第二部分測試了烤這個虛擬熱狗和添加配料的方法。不過從最後幾行代碼來看,我認為烤得太過了。這太浪費番茄醬和芥末醬了!

 


14.7 隱藏數據

你可能已經意識到,查看或修改物件中的資料(屬性)有兩種方法。可以直接訪問,像這樣:

myDog.cooked_level = 5

 

或者也可以使用修改屬性的方法,例如:

myDog.cook(5)

 

如果熱狗開始時是生的(cooked_level = 0),這兩種做法的作用相同。它們都會把 cooked_level 設置為 5。那麼為什麼還要那麼麻煩,專門建立一個方法來做這個工作呢?為什麼不直接修改呢?

我可以想到至少兩個原因。

·    如果直接訪問屬性,烤熱狗至少需要兩部分:改變 cooked_level 和改變 cooked_string。而利用一個方法,可以只做一個方法調用,它就會完成我們需要的一切工作。

·    如果直接訪問屬性,就會有這樣的結果:

cooked_level = cooked_level - 2

這會使熱狗比以前還生。不過熱狗肯定不會越烤越生!所以這是毫無意義的。通過使用方法,可以確保 cooked_level 只會增加而不會減少。

術語箱

按程式設計術語來講,如果限制對物件資料的訪問,使得只能通過使用方法來獲取和修改這些資料,就稱為資料隱藏(data hiding)。Python 沒有提供任何途徑來保證資料隱藏,不過如果你願意,可以適當地編寫代碼來遵循這個規則。

目前為止,我們已經看到物件包含屬性和方法。而且瞭解了如何創建物件以及如何利用一個名為 __init__() 的特殊方法初始化物件。我們還看到了另一個特殊方法 __str__(),利用這個方法可以更好地列印我們的物件。

 

14.8 多態和繼承

接下來,我們來看物件最為重要的兩個方面:多態(polymorphism 和繼承(inheritance)。這兩個詞很長很深奧,不過正是因為有這兩個方面,才使得物件如此有用。我會在下面幾節清楚地解釋它們的含義。

多態——同一個方法,不同的行為

非常簡單,多態是指對於不同的類,可以有同名的兩個(或多個)方法。取決於這些方法分別應用到哪個類,它們可以有不同的行為。

例如,假設你要建立一個程式做幾何題,需要計算不同形狀的面積,比如三角形和正方形。你可以創建兩個類,如下:


Triangle 類和 Square 類都有一個名為 getArea() 的方法。所以,如果分別有這兩個類的實例,如下:

>>> myTriangle = Triangle(4, 5)

>>> mySquare = Square(7)

 

就可以使用 getArea() 分別計算它們的面積:

>>> myTriangle.getArea()

10.0

>>> mySquare.getArea()

49

 

這兩個形狀都使用了方法名 getArea(),不過每個形狀中這個方法做的工作不同。這就是一個多態的例子。

繼承——向父母學習

在真實的(非程式設計)世界中,人們可以從他們的父母或者其他親戚那裡繼承一些東西。你可以繼承一些特徵,比如說紅頭髮,或者可以繼承像錢和財產之類的東西。


在物件導向程式設計中,類可以從其他類繼承屬性和方法。這樣就有了類的整個家族,這個家族中的每個類共用相同的屬性和方法。這樣一來,每次向家族增加新成員時就不必從頭開始。

從其他類繼承屬性或方法的類稱為派生類(derived class)或子類(subclass)。可以舉一個例子來解釋這個概念。

假想我們要建立一個遊戲,玩家一路上可以撿起不同的東西,比如食物、錢或衣服。可以建一個類,名為 GameObjectGameObject 類有 name 等屬性(例如 coinapple hat)和 pickUp() 等方法(它會把硬幣增加到玩家的物品集合中)。所有遊戲物件都有這些共同的方法和屬性。

然後,可以為硬幣建立一個子類。Coin 類從 GameObject 派生。它要繼承 GameObject 的屬性和方法,所以 Coin 類會自動有一個 name 屬性和 pickUp() 方法。Coin 類還需要一個 value 屬性(這個硬幣價值多少)和一個 spend() 方法(可以用這個硬幣去買東西)。

下面來看這些類的代碼:

 


14.9 未雨綢繆

在上面的例子中,我們並沒有在方法中加入任何實際代碼,只有一些注釋來解釋這些方法要做什麼。這是一種未雨綢繆的方法,是對以後要增加的內容提前做出計畫或提前考慮。具體的代碼要取決於遊戲如何工作。程式師編寫比較複雜的代碼時通常就會採用這種做法來組織他們的想法。函數或方法稱為代碼樁(code stub)。

如果想運行前面的例子,會得到一條錯誤消息,因為函式定義不能為空。


沒錯,卡特,不過注釋不起作用,因為它們只是給你讀的,而不是讓電腦來執行。

如果希望建立一個代碼樁,可以使用 Python  pass 關鍵字作為一個預留位置。代碼實際上應該像下面這樣:


我不打算再在這一章中給出使用物件、多態和繼承的更詳細的例子。學習這本書後面的內容時還會看到很多關於物件以及如何使用物件的例子。通過在實際的程式(比如遊戲)中使用物件,你會有更深入的理解。

你學到了什麼

在這一章,你學到了以下內容。

·    什麼是物件。

·    屬性和方法。

·    什麼是類。

·    創建類的一個實例。

·    特殊方法: __init__()  __str__()

·    多態。

·    繼承。

·    代碼樁。

測試題

1. 定義一個新的物件類型時用什麼關鍵字?

2. 什麼是屬性?

3. 什麼是方法?

4. 類和實例之間有什麼區別?

5. 方法中實例引用通常用什麼名字?

6. 什麼是多態?

7. 什麼是繼承?

動手試一試

1.  BankAccount 建立一個類定義。它應該有一些屬性,包括帳戶名(一個字串)、帳號(一個字串或整數)和餘額(一個浮點數),另外還要有一些方法顯示餘額、存錢和取錢。

2. 建立一個可以掙利息的類,名為 InterestAccount。這應當是 BankAccount 的一個子類(所以會繼承 BankAccount 的屬性和方法)。InterestAccount 還應當有一個對應利息率的屬性,另外有一個方法來增加利息。為了力求簡單,假設每年會調用一次 addInterest() 方法計算利息並更新餘額。

 

 


0 留言:

發佈留言