一、什麼是繼承
繼承是一種新建子類的方式,新建的稱為子類/派生類,被繼承的類稱為父類
子類會遺傳父類的屬性,即可以訪問和呼叫父類的屬性
二、為什麼要有繼承
為了解決定義多個類時,程式碼冗餘的問題。當我們在定義多個存在相同屬性與功能的類時,相同程式碼可能會複寫多次,我們可以將這些相同的程式碼抽出來,放到一個公共的類當中,也就是父類當中,其餘類繼承父類,這樣相同程式碼只需寫一遍,並且其餘的類可以用到。
- 在python中可以繼承一個類,也可以繼承多個類
- 在python3中如果一個類沒有繼承任何父類,那麼預設繼承object類
- 但凡是繼承了object類的子類,以及該子類的子子孫孫類都能用到object內的功能,稱之為新式類
- 沒有繼承了object類的子類,以及該子類的子子孫孫類都能用不到object內的功能,稱之為經典類
- ps:只有在python2中才區分經典類與新式類,python3中都是新式類
繼承類的語法
class ParentClass1: #定義父類 pass class ParentClass2: #定義父類 pass class SubClass1(ParentClass1): #單繼承,基類是ParentClass1,派生類是SubClass pass class SubClass2(ParentClass1,ParentClass2): #python支援多繼承,用逗號分隔開多個繼承的類 pass print(SubClass2.__bases__) print(SubClass1.__bases__) print(ParentClass2.__bases__)
我們可以呼叫__bases__()方法檢視某一個類的父類
三、解決程式碼冗餘例項
# 繼承解決程式碼冗餘例項 # 不適用繼承定義老師和學生兩個類 class Student: school = "SH" def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender def student_info(self): print("學生%s:%s %s" % (self.name, self.age, self.gender)) class Teacher: def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender def teacher_info(self): print("老師%s %s %s" % (self.name, self.age, self.gender)) ## 可以看到,我們在定義老師和學生的類時,姓名、年齡、性別寫了重複程式碼,需要優化 # 定義一個成員類來優化上述問題 class Member: school = "SH" def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender class Student(Member): # def __init__(self,name,age,gender): # self.name = name # self.age = age # self.gender = gender def student_info(self): print("學生%s:%s %s" % (self.name, self.age, self.gender)) class Teacher(Member): # def __init__(self,name,age,gender): # self.name = name # self.age = age # self.gender = gender def teacher_info(self): print("老師%s %s %s" % (self.name, self.age, self.gender)) # 生成兩個物件 student1 = Student("zhangdada", 18, "male") teacher1 = Teacher("faxia", 19, "female") print(student1.__dict__) # {'gender': 'male', 'name': 'zhangdada', 'age': 18} print(teacher1.__dict__) # {'gender': 'female', 'name': 'faxia', 'age': 19}
- 在定義父類時,我們應該遵守先抽象,再繼承的原則,即先抽離多個類的相同屬性,再定義父類
- 在繼承父類之後,查詢屬性的順序為:物件自己—>生成物件的類—>父類
四、繼承類後的屬性查詢順序詳解
繼承父類分為兩種形式,單個繼承與多個父類繼承,單個父類相對簡單,多個父類需要考慮多種因素
1、單個父類繼承下的屬性查詢
按照:物件—>子類—>父類—>父類的父類......的順序逐個查詢
2、繼承多個父類的屬性查詢
1)首先了解兩個概念:新式類/經典類、菱形繼承/非菱形繼承
- 但凡是繼承了object類的子類,以及該子類的子子孫孫類都能用到object內的功能,稱之為新式類;沒有繼承了object類的子類,以及該子類的子子孫孫類都能用不到object內的功能,稱之為經典類。Python3中全部是新式類,因為都繼承了object類
- 菱形繼承即一個類繼承了多個類,被繼承的父類又繼承了各自的父類,但是最終,這些繼承的分支又迴歸為一點,繼承了同一個類(object除外),這就是菱形繼承,反之,則是非菱形繼承
2)兩種情況下的屬性查詢
- 非菱形繼承下,無論是新式類還是經典類,都是按照:物件—》類A—》類B—》類C—》類E—》類G—》類D—》類F(—》object,新式類下最後檢索object)
- 在菱形繼承下,新式類遵循廣度優先原則,先找每個分支,但是不會查詢類G,直到所有的類中都沒有找到,最後再找類G,然後是object;物件—》類A—》類B—》類C—》類E—》類D—》類F—》類G—》object
- 在菱形繼承下,經典類按照深度優先,會將一條分支查詢完畢,才會去查詢下一條分支,也就是說會在類E後去查詢類G,之後不再查詢類G:物件—》類A—》類B—》類C—》類E—》類G—》類D—》類F
3)c3演算法
其實,以上的查詢順序都是由c3演算法得出,c3演算法會得出一個mro字典,這個字典規定了屬性查詢的順序,需要注意的是,查詢的順序都是以發起查詢類為起點,無論查詢到什麼位置,順序都不會改變
# c3會計算出一個mro列表 # super()得到一個特殊的物件,該物件訪問的屬性直接從父類中查詢 class A: def test(self): print('A下的test') super().test() class B: def test(self): print('from B') class C(A,B): pass obj = C() # A下的test obj.test() # from B print(C.mro()) # [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
上面的例子展示了c3演算法下的mro字典,但是卻有個坑,相信不少細心的童鞋看出來了,super()方法我們在後面會講到,但是可以提前說明一下作用:自當前類為起點,到他的父類中搜尋,也就是從A的父類中去搜尋test(),但是A並沒有繼承任何父類,卻是成功找到了test(),這是怎麼回事?
我們檢視結果可以看到,obj.test() 的結果為from B,這是B執行test()的結果,難道A將B當做了父類去檢索?事實上確實如此,這裡我們需要注意,因為是從C為起點開始查詢,
所以查詢順序是C的mro字典:[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>],字典中B在A的後面,所以super()將B當做了A的父類去查詢,並最總找到B的test(),執行得到結果。
因為涉及到多父類繼承,並且繼承方式的不同,順序也有區別,所以我們遇到上述情況時,可以檢視mro列表,更好的理解父類間的查詢順序。
五、在子類派生的新方法中如何重用父類的功能
上面講到子類可以繼承父類的屬性,但是子類還有自己的屬性,在子類派生的新方法中如何重用父類的功能呢?下面兩個方法可以實現
- 直接呼叫父類.函式(),與繼承無關
# 在子類派生的新方法中如何重用父類的功能 ## 父類.函式(),與繼承無關 class Member: school = "SH" def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender class Student(Member): def __init__(self,name,age,gender,courses=None): Member.__init__(name,age,gender) # 呼叫父類__init__()方法 if courses == None: courses = [] self.couurses = courses def student_info(self): print("學生%s:%s %s" % (self.name, self.age, self.gender)) class Teacher(Member): def __init__(self,name,age,gender,level): Member.__init__(name, age, gender) # 呼叫父類__init__()方法 self.level = level def teacher_info(self): print("老師%s %s %s" % (self.name, self.age, self.gender))
- 使用super()物件,super()物件會參照屬性查詢發起者的mro列表,去當前類的下一個類中找屬性
# 在子類派生的新方法中如何重用父類的功能 ## 使用super()方法 class Member: school = "SH" def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender class Student(Member): def __init__(self,name,age,gender,courses=None): super().__init__(name, age, gender) # 使用super()方法 if courses == None: courses = [] self.couurses = courses def student_info(self): print("學生%s:%s %s" % (self.name, self.age, self.gender)) class Teacher(Member): def __init__(self,name,age,gender,level): super().__init__(name, age, gender) # 使用super()方法 self.level = level def teacher_info(self): print("老師%s %s %s" % (self.name, self.age, self.gender))
六、隱藏屬性。隱藏屬性的真正目的就是為了不讓使用者在類外部的直接操作屬性
在類中使用__屬性名 可以隱藏屬性————本質是將屬性名進行了變形:_類名__屬性名
class Student: def __init__(self,name,age,gender): self.__name = name # 將屬性變形為:_Student__name self.age = age self.gender = gender def info(self): print(self.__name) # 將屬性變形為:_Student__name print(self.age) print(self.gender) stu1 = Student("zhangdada",21,"male") stu1.name # 類外無法通過屬性名直接訪問
隱藏的屬性有4個特點
- 並不是真正的隱藏,只是一種語法意義上的變形:_類名__屬性名
- 該變形操作只在類定義階段掃描語法時發生一次,類定義之後__開頭的屬性不會變形
- 該隱藏對外不對內。在類的內部,可以通過__屬性名訪問到
- 可以防止子類覆蓋父類的某個屬性
七、繼承表達的關係:is-a及Mixins機制
這其實是一種使用類繼承的思想,我們使用繼承的初衷是為了避免類與類之間的程式碼冗餘,但是過度濫用繼承也會造成程式碼邏輯混亂,影響程式碼的可讀性,那麼我們在使用繼承時,應該遵循什麼原則呢?答案就是is-a原則
- is-a,顧名思義子類是父類的關係:人是動物,狗是動物.....需要注意,這裡的是可以理解為被包含,如果僅是有聯絡就不應該使用繼承,應該使用接下來要講的組合,例如學生和課程,班級和老師等
Mixins機制
從我們的認知當中,一個事物應該只有一個is-a關係,你不能說一個事物是兩種東西(你可能會舉:人是生物,人也是動物的例子,但是動物本就包含在生物之下,我們說人是生物也包含了動物),也就是說我們在使用繼承時,一般只應該有一個父類,但是繼承多個類的情況也是存在的,如果不加以區別,將多個類都認為是一個類的父類,不僅會顯得邏輯混亂,程式碼不夠清晰,而且也會導致菱形繼承等問題,所以python提供了Mixins機制解決這個問題。
舉個例子:我們有汽車、飛機、熱氣球三種類,他們都有一個父類交通工具,這三種類確實是交通工具,沒有問題,但是如果我們的增加了需求,需要新增飛行這個類,怎麼辦?我們直接想到的辦法是定義一個飛行類,然後讓飛機與熱氣球直接繼承飛行這個類,但是這樣違背了is-a的原則,並且出現了繼承多個類的原則。
我們注意到,飛行只是飛機與熱氣球的功能,我們需要的僅僅是功能的混合,而不是繼承一個父類,這時,我們只需要定義一個以Mixins結尾的類,使用飛機與熱氣球繼承這個類,表示這僅僅是功能混合,不是父類,需要注意,這個以Mixins結尾的類需要寫在父類前面。
# 定義類:交通工具 class Vehicle: def __init__(self,name): self.name = name # 定義功能類:飛行 class FlyMixins: def fly(self): pass # 定義類:汽車 class Car(Vehicle): def __init__(self,name): super().__init__() # 定義類:飛機,繼承父類Vehicle以及功能類FlyMixins class Plane(FlyMixins,Vehicle): def __init__(self,name): super().__init__(name) # 定義類:熱氣球,繼承父類Vehicle以及功能類FlyMixins class Balloon(FlyMixins,Vehicle): def __init__(self,name): super().__init__(name)
八,組合
在一個類中以另外一個類的物件作為資料屬性,稱為類的組合。組合與繼承都是用來解決程式碼的重用性問題。不同的是:繼承是一種“是”的關係,比如老師是人、學生是人,當類之間有很多相同的之處,應該使用繼承;而組合則是一種“有”的關係,比如老師有生日,老師有多門課程,當類之間有顯著不同,並且較小的類是較大的類所需要的元件時,應該使用組合,如下示例
# 定義課程類 class Courses: def __init__(self,name,period,price): self.name = name self.period = period self.price = price def check_course(self): print(self.name) # 定義學生類 class Student(People): # 繼承 def __init__(self,name,age,gender,courses=None): super().__init__(name,age,gender) if courses == None: courses = [] self.courses = courses def func1(self): pass # 生成物件學生 stu1 = Student("zhangyifan",20,"male") # 生成課程物件 course1 = Courses("Python","6month",10000) # 將課程物件當做資料屬性傳入學生物件,這就是組合 stu1.courses.append(course1) # 通過學生類訪問課程屬性, print(stu1.courses[0].name) # Python