09-03 繼承與派生

[TOC]

一 繼承介紹

插圖:惡搞圖22
09-03 繼承與派生

繼承是一種創建新類的方式,在Python中,新建的類可以繼承一個或多個父類,新建的類可稱爲子類或派生類,父類又可稱爲基類或超類

class ParentClass1: #定義父類
    pass

class ParentClass2: #定義父類
    pass

class SubClass1(ParentClass1): #單繼承
    pass

class SubClass2(ParentClass1,ParentClass2): #多繼承
    pass

通過類的內置屬性__bases__可以查看類繼承的所有父類

>>> SubClass2.__bases__
(<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)

插圖:惡搞圖23
09-03 繼承與派生

在Python2中有經典類與新式類之分,沒有顯式地繼承object類的類,以及該類的子類,都是經典類,顯式地繼承object的類,以及該類的子類,都是新式類。而在Python3中,即使沒有顯式地繼承object,也會默認繼承該類,如下

>>> ParentClass1.__bases__
(<class ‘object'>,)
>>> ParentClass2.__bases__
(<class 'object'>,)

因而在Python3中統一都是新式類,關於經典類與新式類的區別,我們稍後討論

提示:object類提供了一些常用內置方法的實現,如用來在打印對象時返回字符串的內置方法__str__

插圖:惡搞圖24
09-03 繼承與派生

二 繼承與抽象

要找出類與類之間的繼承關係,需要先抽象,再繼承。抽象即總結相似之處,總結對象之間的相似之處得到類,總結類與類之間的相似之處就可以得到父類,如下圖所示

09-03 繼承與派生

基於抽象的結果,我們就找到了繼承關係
09-03 繼承與派生

基於上圖我們可以看出類與類之間的繼承指的是什麼’是’什麼的關係(比如人類,豬類,猴類都是動物類)。子類可以繼承/遺傳父類所有的屬性,因而繼承可以用來解決類與類之間的代碼重用性問題。比如我們按照定義Student類的方式再定義一個Teacher類

class Teacher:
    school='清華大學'

    def __init__(self,name,sex,age):
        self.name=name
        self.sex=sex
        self.age=age

    def teach(self):
        print('%s is teaching' %self.name)

插圖:惡搞圖33
09-03 繼承與派生

類Teacher與Student之間存在重複的代碼,老師與學生都是人類,所以我們可以得出如下繼承關係,實現代碼重用

class People:
    school='清華大學'

    def __init__(self,name,sex,age):
        self.name=name
        self.sex=sex
        self.age=age

class Student(People):
    def choose(self):
        print('%s is choosing a course' %self.name)

class Teacher(People):
    def teach(self):
        print('%s is teaching' %self.name)

Teacher類內並沒有定義__init__方法,但是會從父類中找到__init__,因而仍然可以正常實例化,如下

>>> teacher1=Teacher('lili','male',18)
>>> teacher1.school,teacher1.name,teacher1.sex,teacher1.age
('清華大學', 'lili', 'male', 18)

插圖:惡搞圖25
09-03 繼承與派生

三 屬性查找

有了繼承關係,對象在查找屬性時,先從對象自己的__dict__中找,如果沒有則去子類中找,然後再去父類中找……

>>> 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()
Foo.f2
Foo.f1

b.f2()會在父類Foo中找到f2,先打印Foo.f2,然後執行到self.f1(),即b.f1(),仍會按照:對象本身->類Bar->父類Foo的順序依次找下去,在類Bar中找到f1,因而打印結果爲Foo.f1

插圖:惡搞圖26

09-03 繼承與派生

父類如果不想讓子類覆蓋自己的方法,可以採用雙下劃線開頭的方式將方法設置爲私有的

>>> class Foo:
...     def __f1(self): # 變形爲_Foo__fa
...         print('Foo.f1') 
...     def f2(self):
...         print('Foo.f2')
...         self.__f1() # 變形爲self._Foo__fa,因而只會調用自己所在的類中的方法
... 
>>> class Bar(Foo):
...     def __f1(self): # 變形爲_Bar__f1
...         print('Foo.f1')
... 
>>> 
>>> b=Bar()
>>> b.f2() #在父類中找到f2方法,進而調用b._Foo__f1()方法,同樣是在父類中找到該方法
Foo.f2
Foo.f1

插圖:惡搞圖27
09-03 繼承與派生

四 繼承的實現原理

對於你定義的每一個類,Python都會計算出一個方法解析順序(MRO)列表,該MRO列表就是一個簡單的所有基類的線性順序列表,如下

>>> F.mro() # 新式類內置了mro方法可以查看線性列表的內容,經典類沒有該內置該方法
[<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, 
<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class ‘object’>]

MRO列表的構造是通過一個C3線性化算法來實現的,我們無需深究該算法的數學原理,它實際上就是合併所有父類的MRO列表,且在查找屬性時,Python會基於MRO列表按照從左到右的順序依次查找基類,直到找到第一個匹配這個屬性的類爲止。

插圖:惡搞圖28
09-03 繼承與派生

在Python中子類可以同時繼承多個父類,在子類繼承了多個父類時,經典類與新式類會有不同MRO,分別對應屬性的兩種查找方式:深度優先和廣度優先

插圖:經典類的深度優先查找
09-03 繼承與派生

插圖:新式類的廣度優先查找
09-03 繼承與派生

五 派生與方法重用

子類可以派生出自己新的屬性,在進行屬性查找時,子類中的屬性名會優先於父類被查找,例如每個老師還有職稱這一屬性,我們就需要在Teacher類中定義該類自己的__init__覆蓋父類的

>>> class People:
...     school='清華大學'
...     
...     def __init__(self,name,sex,age):
...         self.name=name
...         self.sex=sex
...         self.age=age
... 
>>> class Teacher(People):
...     def __init__(self,name,sex,age,title): # 派生
...         self.name=name
...         self.sex=sex
...         self.age=age
...         self.title=title
...     def teach(self):
...         print('%s is teaching' %self.name)
... 
>>> obj=Teacher('lili','female',28,'高級講師') #只會找自己類中的__init__,並不會自動調用父類的
>>> obj.name,obj.sex,obj.age,obj.title
('lili', 'female', 28, '高級講師')

很明顯子類Teacher中__init__內的前三行又是在寫重複代碼,若想在子類派生出的方法內重用父類的功能,有兩種實現方式

方法一:“指名道姓”地調用某一個類的函數

>>> class Teacher(People):
...     def __init__(self,name,sex,age,title):
...         People.__init__(self,name,age,sex) #調用的是函數,因而需要傳入self
...         self.title=title
...     def teach(self):
...         print('%s is teaching' %self.name)
... 

插圖:惡搞圖29
09-03 繼承與派生

方法二:super()

調用super()會得到一個特殊的對象,該對象專門用來引用父類的屬性,且嚴格按照MRO規定的順序向後查找

>>> class Teacher(People):
...     def __init__(self,name,sex,age,title):
...         super().__init__(name,age,sex) #調用的是綁定方法,自動傳入self
...         self.title=title
...     def teach(self):
...         print('%s is teaching' %self.name)
... 

提示:在Python2中super的使用需要完整地寫成super(自己的類名,self) ,而在python3中可以簡寫爲super()。

插圖:惡搞圖30
09-03 繼承與派生

這兩種方式的區別是:方式一是跟繼承沒有關係的,而方式二的super()是依賴於繼承的,並且即使沒有直接繼承關係,super()仍然會按照MRO繼續往後查找

>>> #A沒有繼承B
... class A:
...     def test(self):
...         super().test()
... 
>>> class B:
...     def test(self):
...         print('from B')
... 
>>> class C(A,B):
...     pass
... 
>>> c=C() # C.mro()結果爲 [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<class ‘object'>]
>>> c.test()
from B

c.test()首先找到A下的test方法,執行super().test()會基於MRO列表當前所處的位置繼續往後查找,然後在B中找到了test方法並執行。

關於在子類中重用父類功能的這兩種方式,使用任何一種都可以,但是在最新的代碼中還是推薦使用super()

插圖:惡搞圖31
09-03 繼承與派生

六 組合

在一個類中以另外一個類的對象作爲數據屬性,稱爲類的組合。組合與繼承都是用來解決代碼的重用性問題。不同的是:繼承是一種“是”的關係,比如老師是人、學生是人,當類之間有很多相同的之處,應該使用繼承;而組合則是一種“有”的關係,比如老師有生日,老師有多門課程,當類之間有顯著不同,並且較小的類是較大的類所需要的組件時,應該使用組合,如下示例

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 Date:
    def __init__(self,year,mon,day):
        self.year=year
        self.mon=mon
        self.day=day
    def tell_birth(self):
       print('<%s-%s-%s>' %(self.year,self.mon,self.day))

class People:
    school='清華大學'
    def __init__(self,name,sex,age):
        self.name=name
        self.sex=sex
        self.age=age

#Teacher類基於繼承來重用People的代碼,基於組合來重用Date類和Course類的代碼
class Teacher(People): #老師是人
    def __init__(self,name,sex,age,title,year,mon,day):
        super().__init__(name,age,sex)
        self.birth=Date(year,mon,day) #老師有生日
        self.courses=[] #老師有課程,可以在實例化後,往該列表中添加Course類的對象
    def teach(self):
        print('%s is teaching' %self.name)

python=Course('python','3mons',3000.0)
linux=Course('linux','5mons',5000.0)
teacher1=Teacher('lili','female',28,'博士生導師',1990,3,23)

# teacher1有兩門課程
teacher1.courses.append(python)
teacher1.courses.append(linux)

# 重用Date類的功能
teacher1.birth.tell_birth()

# 重用Course類的功能
for obj in teacher1.courses: 
    obj.tell_info()

此時對象teacher1集對象獨有的屬性、Teacher類中的內容、Course類中的內容於一身(都可以訪問到),是一個高度整合的產物

插圖:惡搞圖32
09-03 繼承與派生

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章