# 第九章 類的定義屬性和方法
## 一、類的定義
### 1、類的概念
Python從設計之初就已經是一門面向對象的語言,正因為如此,在Python中創建一個類和對象是很容易的。你編寫表示現實世界中的事物和情景的類,並基於這些類來創建對象。
編寫類時,你定義一大類對象都有通用行為。基於類創建對象時,每個對象都自動具備這種通用行為,然后可根據需要賦予每個對象獨特的個性。使用面向對象編程可模擬現實情景,其逼真程度達到了驚訝的地步。
在面向對象的世界里,類是用來描述具有相同的屬性和方法的對象的集合。它定義了該集合中每個對象所共有的屬性和方法。你的代碼通常被稱為類的方法,而數據通常稱為類的屬性,實例化的數據對象通常稱為實例。
### 2、類的語法

Python使用class創建類。每個定義的類都有一個特殊的方法,名為__init__(),可以通過這個方法控制如何初始化對象。
## 二、類的屬性和方法
一個對象的特征稱為"屬性",一個對象的行為稱為"方法"。屬性在代碼層面上來看就是變量,方法實際就是函數,通過調用這些函數來完成某些工作。
### 1、創建類和使用類
使用類幾乎可以模擬任何東西。屬性在代碼層面上來看就是變量。下面來編寫一個表示小狗的簡單類Dog--它表示的不是特定的狗,而是任何小狗。對於大多數狗,我們都知道什么呢?大多數狗會蹲下和搖尾巴。由於大多數小狗都具備上述兩項信息(名字和年齡)和兩種行為。我們的Dog類將包含它們。這個類讓Python知道如何創建表示小狗的對象。編寫這個類后,我們將使用它表示特定小狗的實例。
1)、創建Dog類
根據Dog類創建的每一個實例都將存儲名字和年齡,我們賦予了每條小狗蹲下和搖尾巴的能力:
dog.py

在①出,定義了一個名為Dog的類。根據約定,Python中,首字母大寫的名稱指的是類。在②處,編寫了一個文檔字符串,對這個類的功能作了描述。
在④處的方法__init__()是一個特殊的方法,每當你根據Dog類創建新實例時,Python都會自動運行它。在這個方法的名稱中,開頭和結尾各有兩個下划線,這是一種約定,旨在避免Python默認方法和普通方法名稱沖突。在__init__()方法定義了三個形參:self,name和age。這個默認方法必不可少的是形參self,還必須位於其他形參的前面。所以,Python調用這個_init__()方法來創建實例時,將自動傳入這個實參self。讓實例能夠自動訪問類中的屬性和方法。
在⑥、⑦處定義的兩個變量都有前綴self。以self前綴的變量都可提供類中所有方法使用。我們可以通過類的任何實例訪問這些變量。self.name=name獲取存儲在形參name中的值,並將其存儲在變量name中。像這樣通過實例訪問的變量稱為屬性。
Dog類還定義了另外兩種方法:sit()和Wangging(),由於這些方法不需要額外的信息,如名字和年齡,因此他們只有一個形參self。后面創建實例也能夠訪問這些方法。其中(.title())這個方法是表示將屬性name的值的首字母自動變為大寫。
### 2、根據類創建實例
有了類之后,創建對象實例很容易。只需將對類名的調用賦至各個變量,根據需要創建多個對象實例。根據類來創建對象被稱為實例化。
下面來創建一個表示特定小狗的實例:

在①處,創建了一個名字為‘while’、6歲的小狗。通過傳入這兩個實參調用了類中的方法__init__()。方法__init__並沒有含有return語句,但Python自動返回一個表示小狗的實例。我們將這個實例存儲在變量my_dog中。命名約定:通常可以認為首字母大寫的名稱(如Dog)指的就是類,而小寫的名稱(如my_dog)指的根據類創建的實例。
1)、訪問屬性
要訪問實例的屬性,可使用句點表示法。在21行,我們編寫了my_dog.name來訪問my_dog的屬性name的值。在第二條'print'語句中,str(my_dog.age)將my_dog的屬性age的值6轉換為字符串。
執行該代碼,結果為:
---
My dog's name is Willie.
My dog's is 6 years old.
---
2)、調用方法
根據Dog類創建實例后,就可以使用句點表示法來調用Dog類中定義的任何方法。下面來讓小狗蹲下和搖尾巴:

要調用方法,可指定實例的名稱(這里是my_log)和調用的方法,並用句點分割它們。遇到代碼my_dog.sit()時,Python在類Dog中查找sit方法並運行代碼。Python以同樣的方式解讀Wagging()。
執行該代碼,結果為:
---
Willie 會蹲下。
Willie 搖尾巴
---
3)、創建多個實例
可按需求根據類創建任意數量的實例。下面再創建一個名為your_dog實例:

在這個實例中,我們創建了兩條小狗。他們分別名為willie和lucy,每條狗都是一個獨立的實例,有自己的屬性,能夠獲得相同的方法。
執行該代碼,結果為:

### 3、使用類和實例
你可以使用類來模擬現實世界中很多情景。類編寫好,你的大部分時間將花在使用根據類創建的實例上。
首先,類的每個屬性都必須有初始值,哪怕是0或者空字符串。如設置默認值時,在方法__init__)內指定初始值可以,這樣,實例化就不用為它提供初始值的形參了。如果我們狗狗一般每天喝水量為200ml,那么,我們在方法__init__()里增加一個屬性名為water默認值為'200'的屬性。
還是繼續使用Dog.py這個案例,稍微修改成我們需要的案例:

可以看出,在方法__init__()中,新增了第⑧行一個屬性名為water值為'200'的新屬性,這個就是給屬性指定默認值。在⑩行中,我們定義了一個名為message()的方法,那么,實例化對象通過調用message()就能清晰獲悉這個母狗的具體信息。
執行該代碼,結果為:
---
Willie eats 200 of water every day.
Willie is 6 years old.
---
然后,接下來我們的任務是修改實例的屬性,有哪些方式呢?
1)、直接修改屬性的值
要修改屬性的值,最簡單的方式就是通過實例直接訪問它。如果我們將狗狗的性別由母狗改為公狗:
類還是上面的Dog類,實例化對象和調用方法就是以下代碼:

my_dog.water=300直接使用句點法直接訪問狗的水量,讓Python在實例my_dog中找到屬性water,並將該屬性的值設置為300。
執行該代碼,結果為:
---
Willie eats 300ml of water every day.
Willie is 6 years old.
---
2)、通過方法修改屬性的值
如果有替你更新屬性的方法,將大有裨益。這樣,你就無需直接訪問屬性,而可將值傳遞給一個方法,由它在內部更新。

仔細觀察,可以發現,對Dog類所作的唯一修改就是在⑩行新增了方法water_yield(),這個方法接受一個狗的性別參數,並將其存儲在 self.water中。在十九行處,調用了water_yield(),並提供了實參300,然后在message()打印處狗的信息。
執行代碼,結果和直接修改屬性的指輸出的結果一樣。
可對方法water_yield()進行擴展,使其在修改狗的喝水量時能做額外的工作,邏輯上一條狗每天喝水量最好不應該超過默認值200即200ml的水量,如果超過這個水量要給與警告:


如上所示,將方法water_yield()加上條件判斷后,如果調用此方法輸入實參250,那么執行改代碼,結果為:
---
Water exceed the standard!
Willie eats 200ml of water every day.
Willie is 6 years old.
---
傳入水量值為250,判斷條件中,只有小於200才能覆蓋原來的默認值,大於200就沒有改變默認值,並返回一條警告信息。
3)、通過方法對屬性的值進行遞增
那么,如果需要將屬性值遞增特定的量,而不是設置為全新的值。假如某條小狗昨天喝了200ml的水量,今天增加了5ml的水量,下面直接看代碼:

在十一行處,使用+=water進行追加傳入的水量值,靈活去更新默認值。那么,執行該代碼,結果為:

## 三、銷毀方法
與__init__() 方法對應的是__del__()方法__init__()方法用於構造當前類的實例化對象,而__del__() 則用於銷毀實例化對象,即在任何實例化對象將要被系統回收之時,系統都會自動調用該對象的__del__()方法。
當程序不再需要一個Python對象時,系統必須把該對象所占用的內存空間釋放出來,這個過程被稱為垃圾回收,Python會自動回收所有對象所占用的內存空間,因此開發者無須關心對象垃圾回收的過程。
大多數情況下,Python 開發者不需要手動進行垃圾回收,因為 Python 有自動的垃圾回收機制(下面會講),能自動將不需要使用的實例對象進行銷毀。
無論是手動銷毀,還是 Python 自動幫我們銷毀,都會調用__del__()方法。舉個例子:

執行該代碼,結果為:
---
調用__init__()方法構造對象
調用__del__()銷毀對象,釋放其空間
---
但是,千萬不要誤認為,只要為該實例對象調用__del__()方法,該對象所占用的內存空間就會被釋放。舉個例子:

執行該代碼,結果為:
---
調用__init__()方法構造對象
***********
調用__del__() 銷毀對象,釋放其空間
---
注意,最后一行輸出信息,是程序執行即將結束時調用__del__()方法輸出的。可以看到,當程序中有其它變量(比如這里的 cl)引用該實例對象時,即便手動調用__del__()方法,該方法也不會立即執行。這和Python 的垃圾回收機制的實現有關。
Python 采用自動引用計數(簡稱 ARC)的方式實現垃圾回收機制。該方法的核心思想是:每個 Python 對象都會配置一個計數器,初始 Python 實例對象的計數器值都為 0,如果有變量引用該實例對象,其計數器的值會加 1,依次類推;反之,每當一個變量取消對該實例對象的引用,計數器會減 1。如果一個 Python 對象的的計數器值為 0,則表明沒有變量引用該 Python 對象,即證明程序不再需要它,此時 Python 就會自動調用__del__() 方法將其回收。
如果在上面程序結尾,添加如下語句:

執行該代碼,結果為:
---
調用__init__()方法構造對象
***********
調用__del__() 銷毀對象,釋放其空間
+++++
---
可以看到,當執行 del cl 語句時,其應用的對象實例對象 C 的計數器繼續 -1(變為 0),對於計數器為 0 的實例對象,Python 會自動將其視為垃圾進行回收。
## 四、課堂練習
#### 1、

### 2、定義一個類,實例化的時候打印'正在實例化',最后結束的時候,輸出'正在銷毀'。
## 五、上一節課堂練習答案
#### 1、命名一個a列表,然后定義一個函數,函數里有a列表。用案例說明列表是可以在局部被修改;說明列表不能重新賦值;如果需要重新賦值,需要在函數內部使用global定義全局變量。
1)全局列表a可以在局部被修改

執行該代碼,結果為:
---
['global', 'python', 'nonlocal']
['global', 'python', 'nonlocal']
---
發現上面的a並沒有使用galobal但是值卻改變了, 說明列表是可以在局部被修改的。
2)局部變量賦值不能改變全局變量的值

執行該代碼,結果為:
---
nonlocal
['global', 'python']
---
3)使用了global關鍵字后, 變量被重新賦值

執行該代碼,結果為:
---
nonlocal
nonlocal
---
#### 2、計算1到100之間相加之和;通過循環和遞歸兩種方式實現。
1)循環方式

執行該代碼,結果為:
---

---
每一次循環都會輸出結果,缺點是程序繁瑣消耗內存。
2)遞歸方式

執行該代碼,結果為:
---
5050
---
遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。
#### 3、舉一個別人舉過的例子:約會結束后你送你女朋友回家,離別時,你肯定會說:“到家了給我發條信息,我很擔心你。” 對不,然后你女朋友回家以后還真給你發了條信息。小伙子,你有戲了。其實這就是一個回調的過程。你留了個參數函數(要求女朋友給你發條信息)給你女朋友,然后你女朋友回家,回家的動作是主函數。她必須先回到家以后,主函數執行完了,再執行傳進去的函數,然后你就收到一條信息了。

執行該代碼,結果為:
---
我是回調函數:回到家發個信息哦
我是主函數:我回到家啦
---