5th Python面向?qū)ο蠡A(chǔ)
人狗大戰(zhàn)的實例
需求:
多條狗,每個狗有名字,品種,攻擊力
可以有多個人
狗可以咬人,人可以打狗
我們可能會這樣寫:

那我們?nèi)绻卸鄺l狗,我就需要寫多個字典,而且還是要手動創(chuàng)建的,所以我們想到了函數(shù),創(chuàng)建一個dog(name, d_type)
,這樣我們要創(chuàng)建就調(diào)用這個函數(shù)即可,eg:dog('zs', 'big')
,就不用手動創(chuàng)建了。
那隨著產(chǎn)品的功能升級,現(xiàn)在需要狗不能調(diào)用人打狗的動作,人不能調(diào)用狗咬人的動作,我們可以加一個判斷來實現(xiàn);人又分男人和女人,女人可以生孩子,男人不可以,我們就又要講男人和女人的定義函數(shù)分開定義,而且又要新加判斷使生孩子只能女人調(diào)用。。。。。。
可以看出,很麻煩。所以我們需要換一個思路。
5.2 編程范式——面向過程
簡單介紹
面向過程,核心是“過程”這兩個字。我們之前寫的代碼都是面向過程的編程方式(編程范式),就是我們一步一步確定先干什么在什么,然后用函數(shù)一個一個功能的實現(xiàn),最后實現(xiàn)了我們的大目標。
總結(jié)一下,面向過程就是將一個大問題分解成多個小問題然后一一解決。
優(yōu)點
解決問題的流程簡單化,知道一步一步該干嘛,比如Python入門要輸出只需一個print()就好,但如果是Java的話,你得先寫個public static void main這樣的框架。eg:我們要實現(xiàn)修改文件,很容易想到步驟:讀取文件數(shù)據(jù) -> 判斷數(shù)據(jù) -> 數(shù)據(jù)寫入 -> 文件替換。
缺點
通過我們前面寫出來的代碼我們應(yīng)該可以感覺的到,這樣寫出來的程序擴展性不是很強。很多函數(shù)都是與其他函數(shù)牽扯在一起的。函數(shù)一旦寫多,找都找的不方便,也不便于維護,特別是那些需要功能需要經(jīng)常變動,而且需要不斷更新迭代和維護的場景。
應(yīng)用場景
面向過程一般用在代碼更新迭代很少的場景,其他場景最好用面向?qū)ο蟮木幊谭妒健g:Linux內(nèi)核、git等等
5.3 編程范式——面向?qū)ο?/h1>簡單介紹
編程范式最常用的就是面向過程和面向?qū)ο蟆C嫦驅(qū)ο蟮暮诵木褪?span id="s0sssss00s" class="md-pair-s " style="">類與對象。面向?qū)ο缶褪菍嶋H需求抽象成一個個對象,什么是對象?
說對象之前我們要說說類(class),我們可以類似理解為種類,比如人就是一個對象,包括男人、女人、老人、小孩等等等等;動物也是一個對象,包括大象、獅子、兔子等等。
和對象這樣泛指的概念相對應(yīng)的就是對象(object)。對象就是一個指定的一個東西,可以說類是由很多的對象構(gòu)成的。eg:男人是由zm、wzw、yz、zk等等千千萬萬的對象組成的。
然后對象到實例的轉(zhuǎn)化過程就叫做實例化,代碼就和上面提到的代碼很像。eg:Dog('zs', 'big')
。
PS:如果不理解可以類比數(shù)學(xué)中集合和構(gòu)成集合的每個數(shù)來理解。一個類就是一個集合,集合里裝的許多數(shù)就是類下實例化出來的一個個對象。
優(yōu)點
解決了面向過程可擴展性較差的問題,擴展性大大提高。后面的學(xué)習(xí)過程中我們還會再說。
缺點
編程的復(fù)雜度遠高于面向過程,如果不了解面向?qū)ο缶蜕鲜?,再基于面向?qū)ο笤O(shè)計程序,就極容易會出現(xiàn)過度設(shè)計的問題。
應(yīng)用場景
因為面向?qū)ο蟮碾y度,我們在管理Linux系統(tǒng)的shell腳本程序中就會應(yīng)用面向過程的方法。在軟件開發(fā)等方向中面向?qū)ο?/strong>應(yīng)用廣泛。
5.4 類與對象
定義語法
我們先寫一個簡單的類與對象的語法實例:

需要注意的點有:
類中可以寫任何的python代碼,這些代碼在類定義的階段便會執(zhí)行,因而會產(chǎn)生新的名稱空間,用來存放類的變量名與函數(shù)名,可以通過
類名.__dict__
來查看類中定義的名字都是類的屬性,
.
是訪問屬性的語法對于經(jīng)典類來說我們可以通過蓋子點操作類名稱空間的名字,但新式類有限制(經(jīng)典類和新式類的區(qū)別)
類的使用
引用類的屬性:

實例化類,得到一個對象:

__init__
方法:

對象的使用
對象的增刪改查和類的增刪改查很類似。

關(guān)于類的設(shè)置
每個程序員考慮的角度不同時,對于同一個需求定義的類可能截然不同
現(xiàn)實中的類并不完全等于程序中的類,比如人這個類,在程序中可能需要分成老師類、學(xué)生類等等
程序中創(chuàng)建的類可能在現(xiàn)實生活中不存在,比如策略類這個很常用的類,還比如MapReduce中的Mapper和Reducer這兩個核心抽象類
經(jīng)典類和新式類
經(jīng)典類
?class A: # 經(jīng)典類
? ? ?pass
新式類
?class B(object): # 新式類
? ? ?pass
Python中創(chuàng)建的一切都是對象,object可以理解為Python最原始的類,所有對象都是它衍生出來的。
我們一般寫的都是經(jīng)典類,在多繼承那里我們會詳細說它們兩個的區(qū)別。
5.5 實例屬性&公共屬性
我們將上面的Student類修繕一下:

我們創(chuàng)建兩個對象
stu1 = Student('zm', 19, 'male')
stu2 = Student('wzw', 23, 'male')
我們輸出比較id(stu1.school)
和id(stu2.school)
,我們會發(fā)現(xiàn)兩個對象調(diào)用的school屬性的id都是一樣的,我們就稱school這樣的屬性是公有屬性(類屬性)。
那我們在__init__()
中也定義了一些屬性,這里面的屬性我們就稱它們?yōu)閷嵗龑傩裕ㄒ卜Q成員屬性),原因前面我們提過,__init__()
是對象創(chuàng)建過后才將數(shù)據(jù)賦值到屬性上的。
類里面的函數(shù)之間的數(shù)據(jù)是不相關(guān)的,要想實現(xiàn)有相互調(diào)用的數(shù)據(jù),就需要把函數(shù)里面的值存到實例里,那就要將兩個值與實例進行綁定,那我們就用到了self
。
self
代表實例(一個創(chuàng)建的對象,不是類!)本身,在stu1.hello()
調(diào)用實例方法時,他默認的就執(zhí)行stu1.hello(stu1)
,然后函數(shù)中的self.school
就是stu2.school
。那這樣的解釋也可以用來解釋__init__(self)
里面的self.name = name
這樣的語句就是將自己內(nèi)部的數(shù)據(jù)和實例進行綁定,這樣hello()
里面就可以調(diào)用__init__()
里面的name
參數(shù)了。
應(yīng)用情況
一般的,很多對象的屬性都一樣,那就可以用公共屬性。而且公共屬性如果被實例化以后,單獨的實例修改后,就變成了給該實例創(chuàng)建一個新的實例屬性,不會影響到公共屬性的值。常見的屬性有國籍、所屬學(xué)校等等。
像姓名、年齡、性別這些基本每個對象都不一樣的屬性,我們一般用實例屬性。
下面就用上面的Student類做eg:

5.6 類之間的依賴關(guān)系
就像人和人之間存在很多種關(guān)系,類與類之間也有關(guān)系,類與類之間的關(guān)系一般分為下面5種:
依賴關(guān)系:你每個月都找父母要生活費的時候,你和父母的關(guān)系;
關(guān)聯(lián)關(guān)系:你和你基友或者閨蜜;
組合關(guān)系:比聚合關(guān)系更加緊密的關(guān)系,比如大腦和心臟,人死了,所有器官都會死;
聚合關(guān)系:比如電腦的各個部件,電腦壞了有的部件還是正常的;
繼承關(guān)系:類的三大特性之一,子承父業(yè)。
依賴關(guān)系

關(guān)聯(lián)關(guān)系

那我們可以實現(xiàn)一次就雙向關(guān)聯(lián)嘛?
答案是可以的,我們可以將關(guān)聯(lián)關(guān)系單獨放在一個類中,如這一段代碼:

組合關(guān)系
我們接著一開始說的人狗大戰(zhàn),我們增加一個武器類,然后實現(xiàn)武器與人捆綁。

這里武器和人就是組合關(guān)系,武器自己單獨不會被使用,只有創(chuàng)建一個人的對象時,才會被捆綁創(chuàng)建。
可以說,如果兩個對象之間有什么數(shù)據(jù)的交互,那這兩個對象就是組合關(guān)系。
5.7 面向?qū)ο笕筇匦灾^承
繼承就是子承父業(yè),目的就是減少重復(fù)的代碼。具體怎么減少,那我們在代碼中就可以感覺的到。有了繼承的概念就有了派生類的概念,派生類就是子類,只是必須既有父類特性又有自己的一些私有特性。
比如現(xiàn)在我們有人、豬、狗三個類,他們都屬于動物這個類,那我們就可以將人、豬、狗的一些共性寫到動物類的屬性中,這樣就不用寫三遍重復(fù)的代碼了。
繼承的語法是括號:

上面的代碼中,Person和Dog是繼承了Animal類的,這樣Person和Dog就可以不用各自單獨定義__init__()
等相關(guān)重復(fù)代碼了,直接調(diào)用Animal的屬性和方法即可。
在上面的基礎(chǔ)上,子類也可以定義只屬于自己的方法。

那除了這些,子類還可以重寫父類的方法和屬性,這樣也不會影響到父類的方法和屬性。

那我們可以重寫父類的屬性和方法,那父類的__init__()
子類是否可以重寫呢?這點我們單獨放在一點里說,和剛才上面說的有點不一樣。
重寫父類的__init__()
直接上代碼:

代碼中都和大家說的很清楚了,我們只需要注意別忘了給父類的方法傳入self
就好了。
當然,父類__init__()
的重寫不止上面代碼所示的一種方法,上面的方法在python2中經(jīng)常使用,python3常用的方法是通過super()
來重寫。

還有一種super
的寫法:super().__init__(name, age, sex)
,這個相對于第二種比較方便,所以也比較常用。
當然,super()
不會只能調(diào)用父類的__init__()
,父類的所有屬性和方法都可以調(diào)用。調(diào)用的語法是和__init__()
一樣的,比如super().type_animal
、super().say_info()
。
我們可以發(fā)現(xiàn),就是把Animal
換成super()
而已。super()
還有其他的一些應(yīng)用,super()
的用處還在下面要說的多繼承中有所體現(xiàn)。
多繼承
所謂多繼承就是一個類有多個父類,比如如下的代碼:

MonkeyKing這個類在代碼中就是多繼承,同時可以調(diào)用Immortal和Monkey兩個父類的屬性和方法。
在各類主流語言中,C++、Python是支持多繼承的,Java是不支持的。因為多繼承有時可能會使程序調(diào)試變的復(fù)雜。
那多繼承就會引發(fā)一個問題,如果子類調(diào)用父類中有相同名字的方法或者屬性時,到底運行哪一個?比如:

我們看輸出是神仙在打架,我們換一下MonkeyKing后面括號里的繼承順序,就可以看出輸出就會是猴子在打架。我們就可以得出繼承順序是從左到右。
但是多繼承的順序機制沒有這么簡單,我們再改變一下代碼:

那關(guān)系就比較復(fù)雜了,我們該如何確定查找邏輯呢?
這個繼承的關(guān)系我們用圖畫出來就會看出是類似一個樹的結(jié)構(gòu)。我們常見的樹結(jié)構(gòu)的查找邏輯,有深度優(yōu)先算法和廣度優(yōu)先算法。
我們通過注釋輸出類的方法一遍一遍的輸出,我們會發(fā)現(xiàn)繼承順序是Monkey->MonkeyBase->Immortal->Immortal。如果我們再讓Animal作為MonkeyBase的父類,順序就是Monkey->MonkeyBase->Animal->Immortal->Immortal。那我們就可以得出,Python類的繼承順序是按照深度優(yōu)先算法來繼承的。
不過,但這里還沒有結(jié)束。我們再繼續(xù)說說。
我們前面說了經(jīng)典類和新式類。在Python2中,這兩個類的查找法是不一樣的:經(jīng)典類采用深度優(yōu)先查找,而新式類采用廣度優(yōu)先查找。不過到了Python3,無論是經(jīng)典類還是新式類,都是按廣度優(yōu)先查找的。(和上面的結(jié)論沖突,繼續(xù)看C3算法就好)
Python2默認都是經(jīng)典類,Python3默認都是新式類,所以我們不用每次都寫object
,了解了就行。
多繼承的C3算法
不過等等,我們實踐證明出來了python3是深度優(yōu)先,而上面又說是按廣度優(yōu)先,那到底是按照什么來查找?睜著眼說瞎話?
先別急,我們現(xiàn)在再改變一下代碼,讓ImmortalBase和MonkeyBase同時繼承Base類。那我們再重復(fù)剛才的實踐,我們就會發(fā)現(xiàn),順序變成了Monkey->MonkeyBase->Immortal->ImmortalBase->Base。
小朋友你是否有很多問號?????這到底是深度還是廣度?
其實,Python的繼承算法是C3算法,比較復(fù)雜,接下來我們慢慢來介紹。
我們再給出一個繼承關(guān)系復(fù)雜的一段代碼:

我們可以看到輸出的繼承順序是(<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.B2'>, <class '__main__.C2'>, <class 'object'>)
為了較好理解,我們畫成圖來給大家理解:

嗯????小朋友你是否又有很多問號?
到這里,我們只要知道反正肯定不是深度或者廣度,清楚C3對于小白來說也沒什么用,而且C3是Python比較底層的東西了,不用理解的太深入。在Python的官網(wǎng)以及其他的一些資料上都寫了Python是深度優(yōu)先查找,我們知道就好了。
總結(jié)
一般代碼都繼承都不會寫到這么復(fù)雜,不然調(diào)式什么的都不好調(diào)試。
然后還需要提一下的就是,雖然python3默認都是新式類,但是官方的推薦還是要寫object
的,所以我們以后就寫一下就好了,專業(yè)一點。
class A(object):
????pass
5.8 面向?qū)ο笕筇匦灾庋b
封裝是什么
封裝可以被認為是一個保護的屏障,防止該類的代碼和數(shù)據(jù)被外部類定義的代碼隨機訪問。想要訪問就必須通過嚴格的接口控制。
適當?shù)姆庋b可以讓程序代碼更容易被維護,也加強了代碼數(shù)據(jù)的安全性。
封裝的優(yōu)點
良好的封裝能夠減少耦合;
類內(nèi)部的結(jié)構(gòu)可以自由修改;
可以對成員變成進行更精確地控制;
隱藏信息,實現(xiàn)細節(jié)。
封裝的原則
將不需要向外部隨意操作的數(shù)據(jù)進行封裝。
舉些栗子
比如人狗大戰(zhàn)的人和狗的生命值這個屬性,肯定是不能讓外部的代碼隨便修改的,所以我們需要封裝生命值,這些封裝的了屬性就叫私有屬性。
私有屬性不能直接調(diào)用,只能通過類內(nèi)部的一些方法來調(diào)用,這些方法即是前面說到的一些訪問封裝(私有屬性被封裝的東西等)的接口。
外部只能通過接口來操作封裝的東西,你能進行什么操作(刪除、獲取、修改等)全靠類給你提供了什么接口。

封裝的不止可以有屬性,還可以封裝方法,比如我們我們增加一個呼吸。

如果在外部真的想獲取被封裝的東西,語法是實例名._類名+封裝東西的名稱
,可以執(zhí)行新增、修改、獲取、刪除等操作。
p._Person__breath() print(p._Person__life_val)
當然,pycharm可能會警告,但是輸出還是會正常輸出的,只是最好不要這么用,不然你封裝也就沒意義了。
5.9 面向?qū)ο笕筇匦灾鄳B(tài)
什么是多態(tài)
現(xiàn)在有一個動物類Animal,子類有蛇、狗等,他們都繼承Animal的eat()
方法,但是蛇和狗的吃都是不一樣的,比如蛇是吞,狗是一部分一部分的咬。那這種調(diào)用同一個接口,而表示形式不同的現(xiàn)象,就叫做多態(tài)。
統(tǒng)一函數(shù)接口實現(xiàn)多態(tài)

抽象類實現(xiàn)多態(tài)(最常用)

這里的代碼除了raise
那句代碼我們下一章介紹,其他的代碼我們應(yīng)該都可以理解的。多態(tài)的應(yīng)用在網(wǎng)絡(luò)編程的原理普遍用到,雖然我們實際用到的不多,但是確實是一個很重點的點,畢竟面向?qū)ο笕筇攸c之一。
末
這一篇我將成片的代碼段變成了截圖形式,大家看看什么感覺,如果還是習(xí)慣之前灰色處理的話,下一篇就還原回來哈。
有什么Python代碼或者其他問題可以私信問我或者在下面留言,Python課程設(shè)計我也偶爾可以有償幫做,祝大家變得更強[狗頭]
剩下的就是和上一篇文章末尾一樣要說的,我就當成套話了。
套話:因為小破站上的文本格式對演示代碼極其不友好,而且自己平時的筆記是通過Markdown語法來記錄的,在格式上和美觀程度上不是很好看,如果你看的不習(xí)慣,就去下載一個Typora(或者支持markdown語法的應(yīng)用),我這里給出md文件的迅雷和百度網(wǎng)盤鏈接,然后用Typora打開文件看就好了。
迅雷網(wǎng)盤鏈接:https://pan.xunlei.com/s/VMXpt1TptGyPt3YPLs1fEA5eA1
提取碼:s7ei
百度網(wǎng)盤
鏈接:https://pan.baidu.com/s/1jUYcrmv27e6VIO8kVGu9ng
提取碼:1mtr