python語法學習之super(),繼承與派生

1 什麼是繼承?

繼承是一種創建新類的方式;

在Python中,新建的類可以繼承一個或多個父類,新建的類可稱為子類或派生類,父類又可稱為基類或超類。

繼承可以用來解決類與類之間的代碼重用性問題;

class ParentClass1: #定義父類
    pass
class ParentClass2: #定義父類
    pass
class SubClass1(ParentClass1): #單繼承
    pass
class SubClass2(ParentClass1,ParentClass2): #多繼承
    pass

註意:

在python中一個子類可以繼承多個父類,在其他語言中,一個子類隻能繼承一個父類;

python中的繼承分為單繼承和多繼承;

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

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

在Python3中隻有新式類,即使沒有顯式地繼承object,也會默認繼承該類。

2 繼承的規則

子類繼承父類的成員變量和成員方法:

  • 子類不繼承父類的構造方法,能夠繼承父類的析構方法
  • 子類不能刪除父類的成員,但可以重定義父類成員
  • 子類可以增加自己的成員

示例:

# python中子類繼承父類成員變量之間的取值邏輯
class Person():
    def __init__(self, name, age, sex):
        self.name = "jasn"
        self.age = '18'
        self.sex = sex
    def talk(self):
        print("i want to speak something to yo!!")
class Chinese(Person):
    def __init__(self, name, age, sex, language):
        Person.__init__(self, name, age, sex)  # 用父類的name,age,sex 覆蓋掉子類的屬性
        self.age = age  # 覆蓋掉瞭父類的age,取值為子類實例中傳入age參數
        self.language = "chinese"
    def talk(self):
        print("我說的是普通話!!")
        Person.talk(self)
obj = Chinese("nancy",'18','male',"普通話")
print(obj.name)  # 對應場景A
print(obj.age)  # 對應場景B
print(obj.language)  # 對應場景C
obj.talk()  # 對應場景D
# 總結:
# A:若父類中初始化瞭成員變量,子類調用父類構造方法未覆蓋屬性(self.name),則調用子類屬性時取值為父類中初始化的成員變量;
# B:若父類中初始化瞭成員變量,若子類調用父類構造方法覆蓋屬性(self.age)則取值為子類實例中傳入參數
# C:若父類未初始化該成員變量,則無論子類中有無進行對父類構造方法進行屬性的覆蓋,均取子類實例中傳入的參數
# D:對於方法,如果子類有這個方法則直接調用,如果子類沒有則去父類查找。父類沒有則報錯

3 繼承原理

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

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

python會在MRO列表上從左到右開始查找基類,直到找到第一個匹配這個屬性的類為止,而這個MRO列表的構造是通過一個C3線性化算法來實現的。

實際上就是合並所有父類的MRO列表並遵循如下三條準則:

  • 子類會先於父類被檢查
  • 多個父類會根據它們在列表中的順序被檢查
  • 如果對下一個類存在兩個合法的選擇,選擇第一個父

Python 中的 MRO —— 方法搜索順序

Python中針對類提供瞭一個內置屬性 mro 可以查看方法搜索順序

MRO 是 method resolution order,主要用於在多繼承時判斷方法、屬性 的調用路徑。

print(C.__mro__)  #C是多繼承後的類名

輸出結果:

(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

在搜索方法時,是按照 mro 的輸出結果 從左至右 的順序查找的;

如果在當前類中找到方法,就直接執行,不再搜索;如果沒有找到,就查找下一個類中是否有對應的方法,如果找到,就直接執行,不再搜索。

如果找到最後一個類,還沒有找到方法,程序報錯。

4 多繼承屬性查詢順序

(1)多繼承結構為菱形結構

如果繼承關系為菱形結構,那麼經典類與新式類會有不同MRO。

箭頭表示搜索順序

(2)多繼承結構為非菱形結構

會按照先找B這一條分支,然後再找C這一條分支,最後找D這一條分支的順序直到找到我們想要的屬性。

5 查找流程

① 由對象發起的屬性查找,會從對象自身的屬性裡檢索,沒有則會按照對象的類.mro()規定的順序依次找下去。

② 由類發起的屬性查找,會按照當前類.mro()規定的順序依次找下去。

主要知識點:類的__mro__ 屬性的用法;

屬性查找

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

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()
# 運行結果:
Foo.f2
Bar.f1
# 運行流程分析:
b.f2()會在父類Foo中找到f2,先打印Foo.f2,然後執行到self.f1(),即b.f1(),仍會按照:對象本身->類Bar->父類Foo的順序依次找下去,在類Bar中找到f1,因而打印結果為Bar.f1

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

class Foo:
    def __f1(self): # 變形為_Foo__f1
        print('Foo.f1') 
    def f2(self):
        print('Foo.f2')
        self.__f1() # 變形為self._Foo__f1,然後回到Bar類中找,沒有,再到Foo中找到瞭
class Bar(Foo):
    def __f1(self): # 變形為_Bar__f1
        print('Bar.f1')
b=Bar() # Bar類處於執行階段,_Bar__f1變為__f1
b.f2() # 在父類中找到f2方法,進而調用b._Foo__f1()方法,是在父類中找到的f1方法

# 運行結果:
Foo.f2
Foo.f1

6 繼承概念的實現

方式主要有2類:

  • 實現繼承
  • 接口繼承

① 實現繼承是指使用基類的屬性和方法而無需額外編碼的能力。

② 接口繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力(子類重構爹類方法)。

在考慮使用繼承時,有一點需要註意,那就是兩個類之間的關系應該是“屬於”關系。

例如:Employee 是一個人,Manager 也是一個人,因此這兩個類都可以繼承 Person 類,但是 Leg 類卻不能繼承 Person 類,因為腿並不是一個人。

7 私有屬性私有方法在繼承中的表現

父類中的私有方法和私有屬性都是不能被子類繼承下來的;

測試示例:父類中的私有屬性和私有方法是否能被繼承下來?

class Perpon:
    num = 20
    __num1 = 12
    def __test1(self):
        print('__test1....')
    def test2(self):
        print('test2...')
class Student(Perpon):
    def test(self):
        print('num...')
        print(self.num)
        # print(Student.__num1)
        self.test2()
        # self.__test1()
 
student = Student()
student.test()
student.test2()
# student.__test1() # 報錯
'''
num...
20
test2...
test2...
'''

8 派生類

1)在父類的基礎上產生子類,產生的子類就叫做派生類

2)父類裡沒有的方法,在子類中有瞭,這樣的方法就叫做派生方法。

3)父類裡有,子類也有的方法,就叫做方法的重寫(就是把父類裡的方法重寫瞭)

class Hero:
    def __init__(self, nickname,aggressivity,life_value):
        self.nickname = nickname
        self.aggressivity = aggressivity
        self.life_value = life_value
    def attack(self, enemy):
        print('Hero attack')
class Garen(Hero):
    camp = 'Demacia'
    def attack(self, enemy): #self=g1,enemy=r1
        # self.attack(enemy) #g1.attack(r1),這裡相當於無限遞歸
        Hero.attack(self,enemy)  # 引用 父類的 attack,對象會去跑 父類的 attack
        print('from garen attack')  # 再回來這裡
    def fire(self):
        print('%s is firing' % self.nickname)
class Riven(Hero):
    camp = 'Noxus'
g1 = Garen('garen', 18, 200)
r1 = Riven('rivren', 18, 200)
g1.attack(r1)
# print(g1.camp)
# print(r1.camp)
# g1.fire()

9 屬性的覆蓋(派生屬性)

子類也可以添加自己新的屬性或者在自己這裡重新定義這些屬性(不會影響到父類);

需要註意的是:一旦重新定義瞭自己的屬性且與父類重名,那麼調用新增的屬性時,就以自己為準瞭(屬性的覆蓋)。

示例:

# 派生屬性: 子類中自己添加的新屬性
# 屬性的覆蓋: 子類和父類有相同屬性,調用自己的
class Perpon:
    num = 20
    def __init__(self, name):
        print('person...')
class Student(Perpon):
    num = 10  # 把父類中的20覆蓋
    def __init__(self, name, age):  # age 為派生屬性
        super().__init__(name)
        self.name = name
        self.age = age
        print('student...')
    def study(self):
        print(super().num)
        pass
student = Student('趙四', 23)
print(student.name) # person...
print(student.age)
print(student.num)
student.study()
'''
person...
student...
趙四
23
10
20
'''

10 父類屬性(方法)的重用

指名道姓的重用

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)  # 先找到B,B調用A,等這個線性任務處理完之後,在繼續下一行代碼
        C.__init__(self)  # 先找到C,C裡面也調用A的方法
    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().__init__()  # super(D,self).__init__()
f1=D() #super()會基於mro列表,往後找
'''
D的構造方法
B的構造方法
C的構造方法
A的構造方法
'''
# super() 語法
# super(type[, object-or-type])   type 當前類,object-or-type 為實例化對象,一般默認為self,不過該參數在python3中默認

super()是一個特殊的類,調用super得到一個對象,該對象指向父類的名稱空間。

派生與繼承解決問題:子類重用父類的屬性,並派生出新的屬性。

註意:使用哪一種都可以,但不能兩種方式混合使用!

11 繼承派生機制的作用

可以將一些共用的功能加在基類中,實現代碼的共享;

在不改變基類的基礎上改變原有的功能;

練習:

list類裡隻有append向末尾加一個元素的方法,但沒有向列表頭部添加元素的方法,試想能否為列表在不改變原有功能的基礎上添加一個inster_head(x)方法,此方法能在列表的前部添加元素?

class Mylist(list):
    def insert_head(self,x):
        # self.reverse()
        # self.append(x)
        # self.reverse()
        self.insert(0,x)  #直接在最開始插入x
myl = Mylist(range(3,6))
print(myl)      #[3.4.5]
myl.insert_head(2)
print(myl)      #[2,3,4,5]
myl.append(6)
print(myl)      #[2,3,4,5,6]

12 Super()

super(cls,obj)返回被綁定超類的實例(要求obj必須為cls類型的實例)

super() 返回被綁定超類的實例,等同於:super(class,實例方法的第一個參數,必須在方法內調用)

格式:

父類類名.方法名稱(self) 或者 super().方法名稱()或者super(本類類名,對象名)

作用:借助super()返回的實例間接調用父類的覆蓋方法;

示例:

#此示例示意用super函數間接調用父類的
class A:
    def work(self):
        print('A.work被調用')
class B(A):
    '''B類繼承子A類'''
    def work(self):
        print('B.work被調用')
    def super_work(self):
        #調用b類自己的work方法
        self.work()
        #調用父類的work
        super(B,self).work()
        super().work()  #此種調用方式隻能在實例方法內調用
b = B()
# b.work()            #B.work被調用!!
# super(B,b).work()   #A.work被調用
b.super_work()
# super_work()   #出錯

調用super()會得到一個特殊的對象,該對象專門用來引用父類的屬性,且繼承順序嚴格遵循mro繼承序列;

示例:

class Father1:
    x =10
    pass
class Father2:
    x = 20
    pass
#多繼承的情況下,從左到右
class Sub(Father1,Father2):
    def __init__(self):   #註意__int__不是__init__
        print(super().__delattr__)
print(Sub.mro())   # [<class '__main__.Sub'>, <class '__main__.Father1'>, <class '__main__.Father2'>, <class 'object'>]
obj = Sub()
print(object)   #<class 'object'>

mro():會把當前類的繼承關系列出來,嚴格按照mro列表的順序往後查找

class A:        #默認繼承object
    def test(self):
        print('from A.test')
        super().test()
class B:
    def test(self):
        print('from B.test')
class C(A, B):
    pass
c = C()
#檢查super的繼承順序
#mro(): 會把當前類的繼承關系列出來。
print(C.mro()) #[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
c.test()    #from A.test
            #from B.test

使用super調用父類中的方法,註意分析程序的執行順序。

class Parent(object):
    def __init__(self, name, *args, **kwargs):  # 為避免多繼承報錯,使用不定長參數,接受參數
        print('parent的init開始被調用')
        self.name = name
        print('parent的init結束被調用')
class Son1(Parent):
    def __init__(self, name, age, *args, **kwargs):  # 為避免多繼承報錯,使用不定長參數,接受參數
        print('Son1的init開始被調用')
        self.age = age
        super().__init__(name, *args, **kwargs)  # 為避免多繼承報錯,使用不定長參數,接受參數
        print('Son1的init結束被調用')
class Son2(Parent):
    def __init__(self, name, gender, *args, **kwargs):  # 為避免多繼承報錯,使用不定長參數,接受參數
        print('Son2的init開始被調用')
        self.gender = gender
        super().__init__(name, *args, **kwargs)  # 為避免多繼承報錯,使用不定長參數,接受參數
        print('Son2的init結束被調用')
class Grandson(Son1, Son2):
    def __init__(self, name, age, gender):
        print('Grandson的init開始被調用')
        # 多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍
        # 而super隻用一句話,執行瞭全部父類的方法,這也是為何多繼承需要全部傳參的一個原因
        # super(Grandson, self).__init__(name, age, gender) 效果和下面的一樣
        super().__init__(name, age, gender)
        print('Grandson的init結束被調用')
print(Grandson.__mro__) #搜索順序
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年齡:', gs.age)
print('性別:', gs.gender)
 
'''結果如下:
(<class '__main__.Grandson'>, <class '__main__.Son1'>, <class '__main__.Son2'>, <class '__main__.Parent'>, <class 'object'>)
Grandson的init開始被調用
Son1的init開始被調用
Son2的init開始被調用
parent的init開始被調用
parent的init結束被調用
Son2的init結束被調用
Son1的init結束被調用
Grandson的init結束被調用
姓名:grandson
年齡:12
性別:男
'''

註意:在上面模塊中,當在子類中通過super調用父類方法時,parent被執行瞭1次。

super調用過程:上面gs初始化時,先執行grandson中init方法, 其中的init有super調用,每執行到一次super時,都會從__mro__方法元組中順序查找搜索。

所以先調用son1的init方法,在son1中又有super調用,這個時候就就根據__mro__表去調用son2的init,然後在son2中又有super調用,這個就根據mro表又去調用parent中的init,直到調用object中的init。

所以上面的打印結果如此,要仔細分析執行過程。

重點提示:

  • 1)super().__init__相對於類名.init,在單繼承上用法基本無差
  • 2)但在多繼承上有區別,super方法能保證每個父類的方法隻會執行一次,而使用類名的方法會導致方法被執行多次,具體看前面的輸出結果。
  • 3)多繼承時,使用super方法,對父類的傳參數,應該是由於python中super的算法導致的原因,必須把參數全部傳遞,否則會報錯。
  • 4)單繼承時,使用super方法,則不能全部傳遞,隻能傳父類方法所需的參數,否則會報錯。
  • 5)多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍, 而使用super方法,隻需寫一句話便執行瞭全部父類的方法。

這也是為何多繼承需要全部傳參的一個原因

練習一

子類調用自己的方法的時候同時調用父類的方法

class Dog(Animal):
    def sleep(self):
        # 方法一 父類.方法名(對象)
        # Animal.eat(self)
        # 方法二 super(子類,對象名).方法名()
        super(Dog,dog).sleep()
        print('去狗窩')
    def look_door(self):
        print('看門狗')
dog = Dog('哈巴狗',23)
dog.sleep() 
'''
睡覺
去狗窩
'''

練習二

在類的外部調用super()

# 類外部調用super()
dog = Dog('哈巴狗', 23)
super(Dog, dog).sleep() # super(子類,對象名).方法名()
dog.sleep()
'''
睡覺
去狗窩
'''

到此這篇關於python語法學習之super(),繼承與派生的文章就介紹到這瞭,更多相關python 繼承與派生內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: