(二)繼承與派生
一、初識繼承
1,什麼是繼承?
繼承是一種建立新類的方式,新建的類可以繼承一個或多個父類(python 支援多繼承),父類又可稱為基類或超類,新建的類稱為派生類或子類。
子類會“遺傳”父類的屬性,從而解決程式碼重用問題(比如練習中 Garen 與 Riven 類有很多冗餘的程式碼)
python 中類的繼承分為:單繼承和多繼承
class ParentClass1: #定義父類 pass class ParentClass2: #定義父類 pass class SubClass1(ParentClass1): #單繼承,基類是ParentClass1,派生類是SubClasspass class SubClass2(ParentClass1,ParentClass2): #python支援多繼承,用逗號分隔開多個繼承的類 pass
檢視繼承
print(SubClass1.__base__) # (<class '__main__.ParentClass1'>,) # __base__只是檢視從左到右繼承的第一個父類, # __bases__則是檢視所有繼承的父類 print(SubClass2.__bases__) # (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
經典類與新式類
1.只有在python2中才分新式類和經典類,python3中統一都是新式類 2.在python2中,沒有顯式的繼承object類的類,以及該類的子類,都是經典類 3.在python2中,顯式地宣告繼承object的類,以及該類的子類,都是新式類 3.在python3中,無論是否繼承object,都預設繼承object,即python3中所有類均為新式類
提示:如果沒有指定基類,python的類會預設繼承object類,object是所有python類的基類,它提供了一些常見方法(如__str__)的實現。
print(ParentClass1.__bases__) # (<class 'object'>,) print(ParentClass2.__bases__) # (<class 'object'>,)
二、繼承與抽象(先抽象在繼承)
繼承描述的是子類與父類之間的關係,是一種什麼是什麼的關係。要找出這種關係,必須先抽象再繼承。
抽象即抽取類似或者說比較像的部分。
抽象分成兩個層次:
1)將奧巴馬和梅西這倆個物件比較像的部分抽取成類;
2)將人,豬,狗這三個類比較像的部分抽取成父類。
抽象最主要的作用是劃分類別(可以隔離關注點,降低複雜度)
繼承:是基於抽象的結果,通過程式語言去實現它,肯定是先經歷抽象這個過程,才能通過繼承的方式去表達出抽象的結構。
抽象只是分析和設計的過程中,一個動作或者說一種技巧,通過抽象可以得到類。
三、繼承與重用性
1,例子:
==========================第一部分 例如 貓可以:喵喵叫、吃、喝、拉、撒 狗可以:汪汪叫、吃、喝、拉、撒 如果我們要分別為貓和狗建立一個類,那麼就需要為 貓 和 狗 實現他們所有的功能,虛擬碼如下: #貓和狗有大量相同的內容 class 貓: def 喵喵叫(self): print '喵喵叫' def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something class 狗: def 汪汪叫(self): print '喵喵叫' def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something ==========================第二部分 上述程式碼不難看出,吃、喝、拉、撒是貓和狗都具有的功能,而我們卻分別的貓和狗的類中編寫了兩次。如果使用 繼承 的思想,如下實現: 動物:吃、喝、拉、撒 貓:喵喵叫(貓繼承動物的功能) 狗:汪汪叫(狗繼承動物的功能) 虛擬碼如下: class 動物: def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something # 在類後面括號中寫入另外一個類名,表示當前類繼承另外一個類 class 貓(動物): def 喵喵叫(self): print '喵喵叫' # 在類後面括號中寫入另外一個類名,表示當前類繼承另外一個類 class 狗(動物): def 汪汪叫(self): print '喵喵叫' ==========================第三部分 #繼承的程式碼實現 class Animal: def eat(self): print("%s 吃 " %self.name) def drink(self): print ("%s 喝 " %self.name) def shit(self): print ("%s 拉 " %self.name) def pee(self): print ("%s 撒 " %self.name) class Cat(Animal): def __init__(self, name): self.name = name self.breed = '貓' def cry(self): print('喵喵叫') class Dog(Animal): def __init__(self, name): self.name = name self.breed='狗' def cry(self): print('汪汪叫') # ######### 執行 ######### c1 = Cat('小白家的小黑貓') c1.eat() c2 = Cat('小黑的小白貓') c2.drink() d1 = Dog('胖子家的小瘦狗') d1.eat()使用繼承來重用程式碼比較好的例子
在開發程式的過程中,如果我們定義了一個類A,然後又想新建立另外一個類B,但是類B的大部分內容與類A的相同時。
我們不可能從頭開始寫一個類B,這就用到了類的繼承的概念。
通過繼承的方式新建類B,讓B繼承A,B會‘遺傳’A的所有屬性(資料屬性和函式屬性),實現程式碼重用。
class Hero: def __init__(self,nickname,aggressivity,life_value): self.nickname=nickname self.aggressivity=aggressivity self.life_value=life_value def move_forward(self): print('%s move forward' %self.nickname) def move_backward(self): print('%s move backward' %self.nickname) def move_left(self): print('%s move forward' %self.nickname) def move_right(self): print('%s move forward' %self.nickname) def attack(self,enemy): enemy.life_value-=self.aggressivity class Garen(Hero): pass class Riven(Hero): pass g1=Garen('草叢倫',100,300) r1=Riven('銳雯雯',57,200) print(g1.life_value) r1.attack(g1) print(g1.life_value) ''' 執行結果 243 '''子類繼承父類
提示:用已經有的類建立一個新的類,這樣就重用了已經有的軟體中的一部分設定,大大節省了程式設計工作量,這就是常說的軟體重用,不僅可以重用自己的類,也可以繼承別人的,比如標準庫,來定製新的資料型別,這樣就是大大縮短了軟體開發週期,對大型軟體開發來說,意義重大。
注意:像g1.life_value之類的屬性引用,會先從例項中找life_value然後去類中找,然後再去父類中找...直到最頂級的父類,如果最頂級的父類中沒有,則報錯。
!!!再看屬性查詢!!!
class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') self.f1() class Bar(Foo): def f1(self): print('Foo.f1') b=Bar() b.f2()View Code
四、派生
當然子類也可以新增自己新的屬性或者在自己這裡重新定義這些屬性(不會影響到父類),需要注意的是,一旦重新定義了自己的屬性且與父類重名,那麼呼叫新增的屬性時,就以自己為準了。
class Riven(Hero): camp='Noxus' def attack(self,enemy): #在自己這裡定義新的attack,不再使用父類的attack,且不會影響父類。 print('from riven') def fly(self): #在自己這裡定義新的函式屬性,就是派生。 print('%s is flying' %self.nickname)
在子類中,新建的重名的函式屬性,在編輯函式內功能的時候,有可能需要重用父類中重名的那個函式功能,應該是用呼叫普通函式的方式,即:類名.func(),此時就與呼叫普通函式無異了,因此即便是self引數也要為其傳值。
class Riven(Hero): camp='Noxus' def __init__(self,nickname,aggressivity,life_value,skin): Hero.__init__(self,nickname,aggressivity,life_value) #呼叫父類功能 self.skin=skin #新屬性 def attack(self,enemy): #在自己這裡定義新的attack,不再使用父類的attack,且不會影響父類 Hero.attack(self,enemy) #呼叫功能 print('from riven') def fly(self): #在自己這裡定義新的 print('%s is flying' %self.nickname) r1=Riven('銳雯雯',57,200,'比基尼') r1.fly() print(r1.skin) ''' 執行結果 銳雯雯 is flying 比基尼 '''
五、組合與重用性
軟體重用的重要方式除了繼承之外還有另外一種方式,即:組合
組合指的是,在一個類中以另外一個類的物件作為資料屬性,稱為類的組合
class Equip: #武器裝備類 def fire(self): print('release Fire skill') class Riven: # 英雄Riven的類,一個英雄需要有裝備,因而需要組合Equip類 camp='Noxus' def __init__(self,nickname): self.nickname=nickname self.equip=Equip() # 用Equip類產生一個裝備(Equip加括號,例項化物件),賦值給例項的equip屬性 r1=Riven('銳雯雯') r1.equip.fire() # 可以使用組合的類產生的物件所持有的方法 # 結果:release Fire skill
組合與繼承都是有效地利用已有類的資源的重要方式。但是二者的概念和使用場景皆不同。
1,繼承的方式
通過繼承建立了派生類與基類之間的關係,它是一種'是'的關係,比如白馬是馬,人是動物。
當類之間有很多相同的功能,提取這些共同的功能做成基類,用繼承比較好,比如老師是人,學生是人
2,組合的方式
用組合的方式建立了類與組合的類之間的關係,它是一種‘有’的關係,比如教授有生日,教授教python和linux課程,教授有學生s1、s2、s3...
class People: def __init__(self,name,age,sex): self.name=name self.age=age self.sex=sex class Course: def __init__(self,name,period,price): self.name=name self.period=period self.price=price def tell_info(self): print('<%s %s %s>' %(self.name,self.period,self.price)) class Teacher(People): def __init__(self,name,age,sex,job_title): People.__init__(self,name,age,sex) self.job_title=job_title self.course=[] self.students=[] class Student(People): def __init__(self,name,age,sex): People.__init__(self,name,age,sex) self.course=[] dsm = Teacher('大司馬',38,'male','金牌講師') s1 = Student('zixi',18,'male') lol = Course('lol','3mons',2000.0) pubg = Course('pubg','3mons',3000.0) #為老師dsm和學生s1新增課程 dsm.course.append(lol) dsm.course.append(pubg) s1.course.append(lol) #為老師egon新增學生s1 dsm.students.append(s1) #使用 for obj in dsm.course: obj.tell_info() # 課程資訊 """ <lol 3mons 2000.0> <pubg 3mons 3000.0> """例:繼承與組合
當類之間有顯著不同,並且較小的類是較大的類所需要的元件時,用組合比較好
六、介面與歸一化設計
1,什麼是介面?
=================第一部分:Java 語言中的介面很好的展現了介面的含義: IAnimal.java /* * Java的Interface介面的特徵: * 1)是一組功能的集合,而不是一個功能 * 2)介面的功能用於互動,所有的功能都是public,即別的物件可操作 * 3)介面只定義函式,但不涉及函式實現 * 4)這些功能是相關的,都是動物相關的功能,但光合作用就不適宜放到IAnimal裡面了 */ package com.oo.demo; public interface IAnimal { public void eat(); public void run(); public void sleep(); public void speak(); } =================第二部分:Pig.java:豬”的類設計,實現了IAnnimal介面 package com.oo.demo; public class Pig implements IAnimal{ //如下每個函式都需要詳細實現 public void eat(){ System.out.println("Pig like to eat grass"); } public void run(){ System.out.println("Pig run: front legs, back legs"); } public void sleep(){ System.out.println("Pig sleep 16 hours every day"); } public void speak(){ System.out.println("Pig can not speak"); } } =================第三部分:Person2.java /* *實現了IAnimal的“人”,有幾點說明一下: * 1)同樣都實現了IAnimal的介面,但“人”和“豬”的實現不一樣,為了避免太多程式碼導致影響閱讀,這裡的程式碼簡化成一行,但輸出的內容不一樣,實際專案中同一介面的同一功能點,不同的類實現完全不一樣 * 2)這裡同樣是“人”這個類,但和前面介紹類時給的類“Person”完全不一樣,這是因為同樣的邏輯概念,在不同的應用場景下,具備的屬性和功能是完全不一樣的 */ package com.oo.demo; public class Person2 implements IAnimal { public void eat(){ System.out.println("Person like to eat meat"); } public void run(){ System.out.println("Person run: left leg, right leg"); } public void sleep(){ System.out.println("Person sleep 8 hours every dat"); } public void speak(){ System.out.println("Hellow world, I am a person"); } } =================第四部分:Tester03.java package com.oo.demo; public class Tester03 { public static void main(String[] args) { System.out.println("===This is a person==="); IAnimal person = new Person2(); person.eat(); person.run(); person.sleep(); person.speak(); System.out.println("\n===This is a pig==="); IAnimal pig = new Pig(); pig.eat(); pig.run(); pig.sleep(); pig.speak(); } }java 中的 interface
tip:你好,給我開個查詢介面。。。此時的介面指的是:自己提供給使用者來呼叫自己功能的方式\方法\入口
2,為什麼要用介面?
介面提取了一群類共同的函式,可以把介面當做一個函式的集合。
然後讓子類去實現介面中的函式。
這麼做的意義在於歸一化,什麼叫歸一化,就是隻要是基於同一個介面實現的類,那麼所有的這些類產生的物件在使用時,從用法上來說都一樣。
3,歸一化的好處在於:
1)歸一化讓使用者無需關心物件的類是什麼,只需要知道這些物件都具備某些功能就可以了,這極大地降低了使用者的使用難度。
2)歸一化使得高層的外部使用者可以不加區分的處理所有介面相容的物件集合
(i)就好象linux的泛檔案概念一樣,所有東西都可以當檔案處理,不必關心它是記憶體、磁碟、網路還是螢幕(當然,對底層設計者,也可以區分出“字元裝置”和“塊裝置”,然後做出針對性的設計:細緻到什麼程度,視需求而定)。
(ii)再比如:我們有一個汽車介面,裡面定義了汽車所有的功能,然後由本田汽車的類,奧迪汽車的類,大眾汽車的類,他們都實現了汽車介面,這樣就好辦了,大家只需要學會了怎麼開汽車,那麼無論是本田,還是奧迪,還是大眾我們都會開了,開的時候根本無需關心我開的是哪一類車,操作手法(函式呼叫)都一樣。
4,模仿 interface
在python中根本就沒有一個叫做 interface 的關鍵字,如果非要去模仿介面的概念,
可以藉助第三方模組:
http://pypi.python.org/pypi/zope.interface
twisted的twisted\internet\interface.py裡使用zope.interface
文件https://zopeinterface.readthedocs.io/en/latest/
設計模式:https://github.com/faif/python-patterns
也可以使用繼承:
繼承的兩種用途
1,繼承基類的方法,並且做出自己的改變或者擴充套件(程式碼重用):實踐中,繼承的這種用途意義並不是很大,甚至常常是有害的。因為它使得子類與基類出現強耦合。
2,宣告某個子類兼容於某基類,定義一個介面類(模仿java的Interface),介面類中定義了一些介面名(就是函式名)且並未實現介面的功能,子類繼承介面類,並且實現介面中的功能
class Interface:#定義介面Interface類來模仿介面的概念,python中壓根就沒有interface關鍵字來定義一個介面。 def read(self): #定介面函式read pass def write(self): #定義介面函式write pass class Txt(Interface): #文字,具體實現read和write def read(self): print('文字資料的讀取方法') def write(self): print('文字資料的讀取方法') class Sata(Interface): #磁碟,具體實現read和write def read(self): print('硬碟資料的讀取方法') def write(self): print('硬碟資料的讀取方法') class Process(Interface): def read(self): print('程序資料的讀取方法') def write(self): print('程序資料的讀取方法')View Code
上面的程式碼只是看起來像介面,其實並沒有起到介面的作用,子類完全可以不用去實現介面,這就用到了抽象類。
七、抽象類
1,什麼是抽象類?
與 java 不一樣,python 也有抽象類的概念,但是,同樣需要藉助模組實現,抽象類是一個特殊的類,它的特殊之處在於只能被繼承,不能被例項化。
2,為什麼要有抽象類?
如果說類是從一堆物件中抽取相同的內容而來的,那麼抽象類就是從一堆類中抽取相同的內容而來的,內容包括資料屬性和函式屬性。
比如我們有香蕉的類,有蘋果的類,有橙子的類,從這些類抽取相同的內容就是水果這個抽象的類,你吃水果時,要麼是吃一個具體的香蕉,要麼是吃一個具體的橙子......你永遠無法吃到一個叫水果的東西。
從設計角度去看,如果類是從現實物件抽象而來的,那麼抽象類就是基於類抽象而來的。
從實際角度來看,抽象類與普通類的不同之處在於:抽象類中只能有抽象方法(沒有實現功能),該類不能被例項化,只能被繼承,且子類必須實現抽象方法。這一點與介面有點類似,但其實是不同的,下面會說到。
3,在 python 中實現抽象類
# 一切皆檔案 import abc # 利用abc模組實現抽象類 class All_file(metaclass=abc.ABCMeta): all_type = 'file' @abc.abstractmethod # 定義抽象方法,無需實現功能 def read(self): """子類必須定義讀功能""" pass @abc.abstractmethod # 定義抽象方法,無需實現功能 def write(self): """子類必須定義寫功能""" pass # class Txt(All_file): # pass # # t1=Txt() # 報錯,子類沒有定義抽象方法 # 抽象類可以被繼承,但是不能被例項化,並且,子類必須實現抽象功能。 class Txt(All_file): # 子類繼承抽象類,但是必須定義read和write方法 def read(self): print('文字資料的讀取方法') def write(self): print('文字資料的讀取方法') class Sata(All_file): # 子類繼承抽象類,但是必須定義read和write方法 def read(self): print('硬碟資料的讀取方法') def write(self): print('硬碟資料的讀取方法') class Process(All_file): # 子類繼承抽象類,但是必須定義read和write方法 def read(self): print('程序資料的讀取方法') def write(self): print('程序資料的讀取方法') wenbenwenjian = Txt() yingpanwenjian = Sata() jinchengwenjian = Process() # 這樣大家都是被歸一化了,也就是一切皆檔案的思想 wenbenwenjian.read() yingpanwenjian.write() jinchengwenjian.read() print(wenbenwenjian.all_type) print(yingpanwenjian.all_type) print(jinchengwenjian.all_type) """ 文字資料的讀取方法 硬碟資料的讀取方法 程序資料的讀取方法 file file file """利用abc模組實現抽象類
4,抽象類與介面
抽象類的本質還是類,指的是一組類的相似性,包括資料屬性(如all_type)和函式屬性(如read,write),而介面只強調函式屬性的相似性。
抽象類是一個介於類和介面之間的一個概念,同時具備類和介面的部分特性,可以用來實現歸一化設計。
八、繼承實現的原理
1,繼承的順序
在 Java 和 C# 中子類只能繼承一個父類,而 Python 中子類可以同時繼承多個父類,如A(B,C,D)
如果繼承關係為非菱形結構,則會按照先找 B 這一條分支,然後再找 C 這一條分支,最後找 D 這一條分支的順序直到找到我們想要的屬性。
如果繼承關係為菱形結構,那麼屬性的查詢方式有兩種,分別是:深度優先和廣度優先。
class A(object): def test(self): print('from A') class B(A): def test(self): print('from B') class C(A): def test(self): print('from C') class D(B): def test(self): print('from D') class E(C): def test(self): print('from E') class F(D,E): # def test(self): # print('from F') pass f1=F() f1.test() print(F.__mro__) #只有新式才有這個屬性可以檢視線性列表,經典類沒有這個屬性 #新式類繼承順序:F->D->B->E->C->A #經典類繼承順序:F->D->B->A->E->C #python3中統一都是新式類 #pyhon2中才分新式類與經典類繼承順序
2,繼承原理(python 如何實現繼承)
python 到底是如何實現繼承的,對於你定義的每一個類,python 會計算出一個方法解析順序(MRO)列表,這個MRO列表就是一個簡單的所有基類的線性順序列表,例如
print(F.__mro__) # (<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
為了實現繼承,python 會在 MRO 列表上從左到右開始查詢基類,直到找到第一個匹配這個屬性的類為止。
而這個MRO列表的構造是通過一個 C3 線性化演算法來實現的。我們不去深究這個演算法的數學原理,它實際上就是合併所有父類的MRO列表並遵循如下三條準則:
1,子類會先於父類被檢查
2,多個父類會根據它們在列表中的順序被檢查
3,如果對下一個類存在兩個合法的選擇,選擇第一個父類
九、子類中呼叫父類的方法
方法一:指名道姓,即 父類名.父類方法()
class Vehicle: # 定義交通工具類 Country = 'China' def __init__(self,name,speed,load,power): self.name = name self.speed = speed self.load = load self.power = power def run(self): print('開動啦...') class Subway(Vehicle): # 地鐵 def __init__(self,name,speed,load,power,line): # 子類重用父類方法,指名道姓,不依賴繼承 Vehicle.__init__(self,name,speed,load,power) self.line = line def run(self): print('地鐵%s號線歡迎您' % self.line) Vehicle.run(self) # 重用父類方法 line13 = Subway('中國地鐵','180m/s','1000人/箱','電',13) line13.run() """ 地鐵13號線歡迎您 開動啦... """方法一
方法二:super() (依賴繼承)
class Vehicle: # 定義交通工具類 Country = 'China' def __init__(self, name, speed, load, power): self.name = name self.speed = speed self.load = load self.power = power def run(self): print('開動啦...') class Subway(Vehicle): # 地鐵 def __init__(self, name, speed, load, power, line): # super(Subway,self) 就相當於例項本身 在python3中super()等同於super(Subway,self) super().__init__(name, speed, load, power) self.line = line def run(self): print('地鐵%s號線歡迎您' % self.line) super(Subway, self).run() class Mobike(Vehicle): # 摩拜單車 pass line13 = Subway('中國地鐵', '180m/s', '1000人/箱', '電', 13) line13.run() """ 地鐵13號線歡迎您 開動啦... """方法二
注意:兩個方法用哪個都可以,但是最好不要混著用,容易蒙。
瞭解部分:
即使沒有直接繼承關係,super仍然會按照mro繼續往後查詢
# A沒有繼承B,但是A內super會基於C.mro()繼續往後找 class A: def test(self): super().test() class B: def test(self): print('from B') class C(A,B): pass c = C() c.test() # 列印結果:from B print(C.mro()) # [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]View Code
指名道姓與 super() 的區別:
# 指名道姓 class A: def __init__(self): print('A的構造方法') class B(A): def __init__(self): print('B的構造方法') A.__init__(self) class C(A): def __init__(self): print('C的構造方法') A.__init__(self) class D(B,C): def __init__(self): print('D的構造方法') B.__init__(self) C.__init__(self) pass f1 = D() # A.__init__被重複呼叫 ''' D的構造方法 B的構造方法 A的構造方法 C的構造方法 A的構造方法 '''指名道姓
# 使用super() class A: def __init__(self): print('A的構造方法') class B(A): def __init__(self): print('B的構造方法') super(B,self).__init__() class C(A): def __init__(self): print('C的構造方法') super(C,self).__init__() class D(B,C): def __init__(self): print('D的構造方法') super(D,self).__init__() f1 = D() # super()會基於mro列表,往後找 print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) ''' D的構造方法 B的構造方法 C的構造方法 A的構造方法 '''super()
當你使用 super() 函式時,Python 會在 MRO 列表上繼續搜尋下一個類,只要每個重定義的方法統一使用 super() 並只調用它一次,那麼控制流最終會遍歷完整個 MRO 列表,每個方法也只會被呼叫一次(注意:使用 super() 呼叫的所有屬性,都是從 MRO 列表當前的位置往後找,千萬不要通過看程式碼去找繼承關係,一定要看 MRO 列表)
練習(加深印象):
# A沒有繼承B,但是A內super會基於C.mro()繼續往後找 class A: def test(self): print('A---->test') super().aaa() class B: def test(self): print('B---->test') def aaa(self): print('B---->aaa') class C(A,B): def aaa(self): print('C----->aaa') c = C() c.test() #列印結果: ''' A---->test B---->aaa ''' print(C.mro()) """ C 作為方法呼叫(即c.test())的發起者,方法呼叫過程中涉及的屬性查詢都參考C.mro()。 父子關係按照mro列表為準,千萬不要從程式碼層面看父子。 例如c.test(),發起者C.mro()列表如下, 從列表中可以看出,B類就是A類他爹,而程式碼層面二者並無繼承關係 [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>] """View Code