python之繼承

貓咪也會碼程式碼發表於2021-04-15

一、什麼是繼承

  繼承是一種新建子類的方式,新建的稱為子類/派生類,被繼承的類稱為父類

  子類會遺傳父類的屬性,即可以訪問和呼叫父類的屬性

二、為什麼要有繼承

  為了解決定義多個類時,程式碼冗餘的問題。當我們在定義多個存在相同屬性與功能的類時,相同程式碼可能會複寫多次,我們可以將這些相同的程式碼抽出來,放到一個公共的類當中,也就是父類當中,其餘類繼承父類,這樣相同程式碼只需寫一遍,並且其餘的類可以用到。

  •   在python中可以繼承一個類,也可以繼承多個類
  •   在python3中如果一個類沒有繼承任何父類,那麼預設繼承object類
  •   但凡是繼承了object類的子類,以及該子類的子子孫孫類都能用到object內的功能,稱之為新式類
  •   沒有繼承了object類的子類,以及該子類的子子孫孫類都能用不到object內的功能,稱之為經典類
  •   ps:只有在python2中才區分經典類與新式類,python3中都是新式類

繼承類的語法

python之繼承
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__)
View Code

我們可以呼叫__bases__()方法檢視某一個類的父類

三、解決程式碼冗餘例項

python之繼承
# 繼承解決程式碼冗餘例項

# 不適用繼承定義老師和學生兩個類
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}
View Code
  • 在定義父類時,我們應該遵守先抽象,再繼承的原則,即先抽離多個類的相同屬性,再定義父類
  • 在繼承父類之後,查詢屬性的順序為:物件自己—>生成物件的類—>父類

四、繼承類後的屬性查詢順序詳解

繼承父類分為兩種形式,單個繼承與多個父類繼承,單個父類相對簡單,多個父類需要考慮多種因素

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字典,這個字典規定了屬性查詢的順序,需要注意的是,查詢的順序都是以發起查詢類為起點,無論查詢到什麼位置,順序都不會改變

python之繼承
# 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'>]
View Code

  上面的例子展示了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列表,更好的理解父類間的查詢順序。

 

五、在子類派生的新方法中如何重用父類的功能

  上面講到子類可以繼承父類的屬性,但是子類還有自己的屬性,在子類派生的新方法中如何重用父類的功能呢?下面兩個方法可以實現

  • 直接呼叫父類.函式(),與繼承無關
python之繼承
# 在子類派生的新方法中如何重用父類的功能

## 父類.函式(),與繼承無關
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))
View Code
  • 使用super()物件,super()物件會參照屬性查詢發起者的mro列表,去當前類的下一個類中找屬性
python之繼承
# 在子類派生的新方法中如何重用父類的功能

## 使用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))
View Code

 

六、隱藏屬性。隱藏屬性的真正目的就是為了不讓使用者在類外部的直接操作屬性

在類中使用__屬性名 可以隱藏屬性————本質是將屬性名進行了變形:_類名__屬性名

python之繼承
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                         # 類外無法通過屬性名直接訪問
View Code

 隱藏的屬性有4個特點

  • 並不是真正的隱藏,只是一種語法意義上的變形:_類名__屬性名
  • 該變形操作只在類定義階段掃描語法時發生一次,類定義之後__開頭的屬性不會變形
  • 該隱藏對外不對內。在類的內部,可以通過__屬性名訪問到
  • 可以防止子類覆蓋父類的某個屬性

七、繼承表達的關係:is-a及Mixins機制

  這其實是一種使用類繼承的思想,我們使用繼承的初衷是為了避免類與類之間的程式碼冗餘,但是過度濫用繼承也會造成程式碼邏輯混亂,影響程式碼的可讀性,那麼我們在使用繼承時,應該遵循什麼原則呢?答案就是is-a原則

  • is-a,顧名思義子類是父類的關係:人是動物,狗是動物.....需要注意,這裡的是可以理解為被包含,如果僅是有聯絡就不應該使用繼承,應該使用接下來要講的組合,例如學生和課程,班級和老師等

Mixins機制

  從我們的認知當中,一個事物應該只有一個is-a關係,你不能說一個事物是兩種東西(你可能會舉:人是生物,人也是動物的例子,但是動物本就包含在生物之下,我們說人是生物也包含了動物),也就是說我們在使用繼承時,一般只應該有一個父類,但是繼承多個類的情況也是存在的,如果不加以區別,將多個類都認為是一個類的父類,不僅會顯得邏輯混亂,程式碼不夠清晰,而且也會導致菱形繼承等問題,所以python提供了Mixins機制解決這個問題。

  舉個例子:我們有汽車、飛機、熱氣球三種類,他們都有一個父類交通工具,這三種類確實是交通工具,沒有問題,但是如果我們的增加了需求,需要新增飛行這個類,怎麼辦?我們直接想到的辦法是定義一個飛行類,然後讓飛機與熱氣球直接繼承飛行這個類,但是這樣違背了is-a的原則,並且出現了繼承多個類的原則。

  我們注意到,飛行只是飛機與熱氣球的功能,我們需要的僅僅是功能的混合,而不是繼承一個父類,這時,我們只需要定義一個以Mixins結尾的類,使用飛機與熱氣球繼承這個類,表示這僅僅是功能混合,不是父類,需要注意,這個以Mixins結尾的類需要寫在父類前面。

python之繼承
# 定義類:交通工具
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)
View Code

 

八,組合

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

python之繼承
# 定義課程類
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
View Code

 

相關文章