1. 程式人生 > >類的三大特徵之——繼承

類的三大特徵之——繼承

昨天我們學習了面向物件程式設計、類與物件。昨天遺漏了一個小知識點就是:在python中,一切皆物件。

首先,我們要來說明,在python3中,統一了類與型別的概念,即在python3中,類就是型別,型別就是類。

我們通過type來檢視一個數據的型別,我們也可以通過type來檢視一個物件的型別,然後我們發現,他們的型別都是class(類)依舊是說,我們平時使用的所有資料型別,列表啊,字典啊等等都是一個物件,由類產生的物件。

那麼現在再去理解物件這個東西會不會更好理解了一些呢,物件不僅得到了資料,還得到了對資料進行操作的一些方法,例如:獲取一個列表物件後,其下的一些方法如append等。

今天我們來說一說類的三大特徵之一:繼承

首先,什麼是繼承

繼承的意思想必大家都知道,我們疑惑的是,在python中,繼承是什麼,它和現實中的繼承有沒有區別。那麼,首先我要告訴你,在python中,繼承是類的獨有的方法。繼承是建立類的一種方式。python支援多繼承,也就是說,一個類可以繼承任意數量的父類。父類又稱為基類或超類,新建的類稱為子類或派生類。

為什麼要有繼承

繼承可以減少程式碼冗餘。(後面會講到為什麼繼承可以減少程式碼冗餘)

怎麼用繼承

昨天我們講定義一個類是通過class 類名: 這樣去定義的。在定義類的時候,也是可以加括號的,括號內寫入一個或多個類名,這樣就是新建的類繼承了括號內的類。

我們用程式碼來示例一下:

class A:                # 定義一個類A
    name = 'A'          # 類A中有一個屬性叫name = 'A'


class B:                # 定義一個類B
    age = 18            # 類B中有一個屬性叫age = 18


class C(A):             # 定義一個類C,繼承類A
    pass


class D(A, B):          # 定義一個類D,同時繼承類A與類B
    pass


obj1 = C()              # 生成一個類C的物件
print(obj1.name)       

我們昨天講過一個屬性的查詢,我們來看,物件obj內沒有name這個屬性,類C中也沒有name屬性。類C的父類A中有name屬性,所以通過obj也可以查詢到name這個屬性。講到這裡大家應該基本可以明白了吧。繼承就是:子類會“”遺傳”父類的屬性。

那麼我們應該如何在程式碼中定義一個父類/基類呢?我們可以通過現實中的例子來看我們應該如何定義出一個父類

繼承描述的是子類與父類之間的關係,是一種什麼是什麼的關係。要找出這種關係,必須先抽象再繼承

我們繼承的目的是為了減少程式碼冗餘,也就是說,當兩個類中出現了相同的程式碼,我們應該將相同的程式碼部分提取出來,定義為父類,然後再由這兩個類去分別繼承該父類。通俗點來說:父類的屬性更普通,子類繼承父類的普通屬性以後,會定義自己與其他類不同的屬性。我們定義父類的目的就是將子類與子類之間的相同程式碼寫一遍,減少程式碼冗餘。

程式碼示例:

class Animal:                  
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        print('eatting')

    def drink(self):
        print('drinking')


class Cat(Animal):
    def yell(self):
        print('喵')


class Dog(Animal):
    def yell(self):
        print('汪')

類Cat和類Dog都有吃與喝的屬性,都有名字和年齡的屬性,所以我們把相同的部分拉出來,定義為父類Animal,這樣子,我們就減少了程式碼冗餘。

學會了類的繼承,我們要關注一個重點:屬性查詢順序

class Foo:
    def f1(self):
        print('Foo.f1')

    def f2(self):
        print('Foo.f2')
        self.f1()


class Bar(Foo):
    def f1(self):
        print('Bar.f1')


b = Bar()
b.f2()

這裡的b.f2()最終的執行結果是 Foo.f2  \n  Bar.f1

這是非常重要的知識點:我們來按照順序看一看,首先,我們通過類Bat獲取一個物件b。通過物件b去呼叫f2這個繫結方法。先在物件自己的名稱空間中查詢,自己名稱空間中沒有則去類的名稱空間中查詢,類的名稱空間中也沒有,則去該類的父類的名稱空間中查詢,在父類的名稱空間中,我們找到了f2,然後執行f2,這時,物件b是當做引數self傳入進來,傳入以後,執行f2函式,第一行列印Foo.f2,然後執行self.f1()也就是執行b.f1()再次從物件自己的名稱空間中查詢,沒有則去類中查詢,在類中我們就可以找到f1了。所以結果是上面所說的結果。大家一定要對屬性的查詢順序銘記在心。這是非常重要的知識點。

當然子類也可以新增自己新的屬性或者在自己這裡重新定義這些屬性(不會影響到父類),需要注意的是,一旦重新定義了自己的屬性且與父類重名,那麼呼叫新增的屬性時,就以自己為準了。

新式類與經典類概念: python2與python3在繼承上的區別:

新式類:但凡繼承object類的子類,以及該子類的子子類,...都稱之為新式類

經典類:沒有繼承object類的子類,以及該子類的子子類,...都稱之為經典類

我們來想一下有沒有這樣一種場景:

父類中定義了一個屬性,子類中需要給該屬性增加一些東西,就目前的知識來說,我們只能重新在子類中定義,這樣就又出現了重複程式碼,程式碼冗餘又出現了,那麼現在我們就來學習一下如何解決在子類中重用父類的屬性

子類中重用父類屬性有兩種方式:

方式一:

class OldboyPeople:
    school = 'Oldboy'

    def __init__(self, name, age, gender):   # 父類中定義init
        self.name = name
        self.age = age
        self.gender = gender


# print(OldboyPeople.__init__)

class OldboyStudent(OldboyPeople):        # 這個子類與父類的init完全一樣,所以可以直接使用
    # def __init__(self, name, age, gender):
    #     self.name = name                    
    #     self.age = age
    #     self.gender = gender

    def choose_course(self):
        print('%s is choosing course' % self.name)


class OldboyTeacher(OldboyPeople):     # 該子類中的init比父類多了兩個引數
    def __init__(self, name, age, gender, level, salary):
        # self.name = name
        # self.age = age
        # self.gender = gender
        OldboyPeople.__init__(self, name, age, gender)   # 直接呼叫父類下的init函式
          # 注意這裡是直接呼叫的函式,不再是繫結方法
        self.level = level
        self.salary = salary

    def score(self, stu, num):
        stu.num = num
        print('老師%s給學生%s打分%s' % (self.name, stu.name, num))

方式一是指名道姓地訪問某一個類中的函式,與繼承沒有太大的關係。

方式二:

class OldboyPeople:       # 父類
    school = 'Oldboy'
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

class OldboyTeacher(OldboyPeople):
    def __init__(self, name, age, gender,level,salary):
        # OldboyPeople.__init__(self, name, age, gender) # 這是方式一的用法
        super(OldboyTeacher,self).__init__(name, age, gender)
        # 在python3中,super函式不需要傳遞引數,為了學習所以將引數寫上去了。
        self.level=level
        self.salary=salary

    def score(self,stu,num):
        stu.num=num
        print('老師%s給學生%s打分%s' %(self.name,stu.name,num))

方式二:super(OldboyTeacher,self),在python3中super可以不傳引數,呼叫該函式會得到一個特殊的物件,該物件是專門用來訪問父類中屬性。

強調:super會嚴格參照類的mro列表依次查詢屬性!!!

上面我們提到了一個叫mro列表的東西,那麼它到底是個什麼呢?聽我給你慢慢道來

我們來詳細講一下類的繼承中,屬性的查詢順序!(重點)

在單繼承背景下,無論是新式類還是經典類屬性查詢順序都一樣(上面提到過新式類與經典類的概念)先檢視物件自己的名稱空間,找不到再找類的名稱空間,找不到再找父類的名稱空間,這樣依次找下去。

在Java和C#中子類只能繼承一個父類,而Python中子類可以同時繼承多個父類,如A(B,C,D),如果繼承關係為非菱形結構,則會按照先找B這一條分支,然後再找C這一條分支,最後找D這一條分支的順序直到找到我們想要的屬性;如果繼承關係為菱形結構,那麼屬性的查詢方式有兩種,分別是:深度優先和廣度優先

深度優先:沿著一條支線走到頭再走下一條支線

廣度優先:沿著一條支線走到頭的前一步,判斷這個頭是否還有別的支線沒有走,直到該頭的所有支線走完才會走這個頭

python到底是如何實現繼承的,對於你定義的每一個類,python會計算出一個方法解析順序(mro)列表,這個mro列表就是一個簡單的所有基類的線性順序列表,mro列表是基於C3演算法得到的。

為了實現繼承,python會在MRO列表上從左到右開始查詢基類,直到找到第一個匹配這個屬性的類為止。而這個MRO列表的構造是通過一個C3線性化演算法來實現的。我們不去深究這個演算法的數學原理,它實際上就是合併所有父類的MRO列表並遵循如下三條準則:

1.子類會先於父類被檢查;

2.多個父類會根據它們在列表中的順序被檢查;

3.如果對下一個類存在兩個合法的選擇,選擇第一個父類

在子類中重用父類屬性的兩種方法介紹完了,使用哪種方法都是可以的,但是!!!千萬不要兩種方法混合使用!!!

還有,在程式中,能不使用多繼承就不要使用多繼承!