Python編程思想(27):類的繼承

李寧老師已經在「極客起源」 微信公衆號推出《Python編程思想》電子書,囊括了Python的核心技術,以及Python的主要函數庫的使用方法。讀者可以在「極客起源」 公衆號中輸入 160442 開始學習。

《Python編程思想》總目錄
《Python編程思想》專欄

繼承是面向對象的3大特徵之一(另兩個特性是封裝和組合),也是實現軟件複用的重要手段。Python的繼承是多繼承機制,也就是一個子類可以同時有多個直接父類。

1. 繼承的語法

Python子類繼承父類的語法是在定義子類時,將多個父類放在子類之後的圓括號中。語法格
式如下:

class SubClass(SuperClassl, SuperClass2,..)
     # 類定義部分

從上面的語法格式來看,定義子類的語法非常簡單,只需在原來的類定義後增加圓括號,並在圓括號中添加多個父類,即可表明該子類繼承了這些父類。

如果在定義一個 Python類時並未顯式指定這個類的直接父類,則這個類默認繼承 object類。因此,object類是所有類的父類,要麼是其直接父類,要麼是其間接父類。

實現繼承的類被稱爲子類,被繼承的類被稱爲父類,也被稱爲基類、超類。父類和子類的關係是一般和特殊的關係。例如水果和香蕉的關係,香蕉繼承了水果,香蕉是水果的子類,則香蕉是種特殊的水果。

由於子類是一種特殊的父類,因此父類包含的範圍總比子類包含的範圍要大,所以可以認爲父類是大類,而子類是小類。

從實際意義上看,子類是對父類的擴展,子類是一種特殊的父類。從這個意義上看,使用繼承來描述子類和父類的關係是不準確的,用擴展更恰當。因此,這樣的說法更加準確: Banana類擴展了Fruit類。

從子類的角度來看,子類擴展( extend)了父類,但從父類的角度來看,父類派生(derive)出子類。也就是說,擴展和派生所描述的是同一個動作,只是觀察角度不同而已。

下面程序示範了子類繼承父類的特點。下面是 Fruit類的代碼。
示例代碼:inherit.py

class Fruit:
    def info(self):
        print(f"我是水果!重{self.weight}克" )

class Food:
    def taste(self):
        print("不同食物的口感不同")

# 定義Banana類,繼承了Fruit和Food類
class Banana(Fruit, Food):
    pass

# 創建Banana對象
b = Banana()
b.weight = 16
# 調用Banana對象的info()方法
b.info()
# 調用Banana對象的taste()方法
b.taste()

這段代碼開始定義了兩個父類:Fruit類和Food類,接下來程序定義了一個 Banana類,該 Banana類是一個空類。

在主程序部分,主程序創建了 Banana對象之後,可以訪問Banana對象的info()和 taste()方法,這表明 Banana對象也具有了info和 taste方法,這就是繼承的作用:子類擴展(繼承)了父類。在子類中將可以繼承得到父類定義的方法,這樣子類就可複用父類的方法了。

2. 關於多繼承

大部分面向對象的編程語言(除了C++)都只支持單繼承,而不支持多繼承,這是由於多繼承不僅增加了編程的複雜度,而且很容易導致一些莫名的錯誤。

Python雖然在語法上明確支持多繼承,但建議如果不是很有必要,則儘量不要使用多繼承,而是使用單繼承,這樣可以保證編程思路更清晰,而且可以避免很多麻煩。

當一個子類有多個直接父類時,該子類會繼承得到所有父類的方法,這一點在前面代碼中已經演示了。現在的問題是,如果多個父類中包含了同名的方法,此時會發生什麼呢?此時排在前面的父類中的方法會“遮蔽”排在後面的父類中的同名方法。

示例代碼: multiple_inherit.py

class Item:
    def info (self):
        print("Item中方法:", '這是一個商品') 
class Product:
    def info (self):
        print("Product中方法:", '這是一個移動產品')
class Mouse1(Item, Product):
    pass
class Mouse2(Product, Item):
    pass
m1 = Mouse1()
m1.info()

m2 = Mouse2()
m2.info()

在這段代碼中讓 Mouse1繼承了Item類和 Product類,Mouse2繼承了Product類和Item類,這兩個類的父類繼承順序是相反的。由於Mouse1類的Item排在前面,因此Item中定義的方法優先級更高, Python會優先到Item父類中搜尋方法,一旦在Item父類中搜尋到目標方法,Python就不會繼續向下搜尋了。

上面程序中Item和 Product兩個父類中都包含了info()方法,當 Mouse1子類對象調用info方法時,子類中沒有定義info方法,因此 Python會從父類中尋找info方法,此時優先使用第1個父類Item中的info方法。而Mouse2子類對象調用的info()方法屬於Product類。

運行上面程序,將看到如下輸出結果:

Item中方法: 這是一個商品
Product中方法: 這是一個移動產品

3. 重寫父類的方法

子類擴展了父類,子類是一種特殊的父類。大部分時候,子類總是以父類爲基礎,額外增加新的方法。但有一種情況例外:子類需要重寫父類的方法。例如鳥類都包含了飛翔方法,其中鴕鳥是種特殊的鳥類,因此鴕鳥應該是鳥的子類,它也將從鳥類獲得飛翔方法,但這個飛翔方法明顯不適合鴕鳥,因此,鴕鳥需要重寫鳥類的方法。
示例代碼:override.py

class Bird: 
    # Bird類的fly()方法
    def fly(self):
        print("我在天空裏自由自在地飛翔...")
class Ostrich(Bird):
    # 重寫Bird類的fly()方法
    def fly(self):
        print("我只能在地上奔跑...")
  
# 創建Ostrich對象
os = Ostrich()
# 執行Ostrich對象的fly()方法,將輸出"我只能在地上奔跑..."
os.fly()

運行上面程序,將看到運行os.fly()時執行的不再是Bird類的fly()方法,而是 Ostrich類的fly()方法。

這種子類包含與父類同名的方法的現象被稱爲方法重寫(Override),也被稱爲方法覆蓋。可以說子類重寫了父類的方法,也可以說子類覆蓋了父類的方法。

4. 使用未綁定方法調用被重寫的方法

如果在子類中調用重寫之後的方法,Python總是會執行子類重寫的方法,不會執行父類中被重寫的方法。如果需要在子類中調用父類中被重寫的實例方法,那該怎麼辦呢?

讀者別忘了,Python類相當於類空間,因此 Python類中的方法本質上相當於類空間內的函數。所以,即使是實例方法,Python也允許通過類名調用。區別在於,在通過類名調用實例方法時,Python不會爲實例方法的第1個參數self自動綁定參數值,而是需要程序顯式綁定第一個參數self。

示例代碼:invoke_parent. py

class BaseClass:
    def name (self):
        print('父類中定義的name方法')
class SubClass(BaseClass):
    # 重寫父類的name方法
    def name (self):
        print('子類重寫父類中的name方法')
    def process (self):
        print('執行process方法')
        # 直接執行name方法,將會調用子類重寫之後的name()方法
        self.name()
        # 使用類名調用實例方法調用父類被重寫的方法
        BaseClass.name(self)
sc = SubClass()
sc.process()

上面程序中 SubClass繼承了 BaseClass類,並重寫了父類的name()方法。接下來程序在 SubClass類中定義了process()方法,該方法直接通過self調用name方法, Python將會執行子類重寫之後的name方法。後面的代碼通過顯式調用 Base_Class 中的name方法,並顯式爲第1個參數self綁定參數值,這就實現了調用父類中被重寫的方法。

5. 使用 super函數調用父類的構造方法

Python的子類也會繼承得到父類的構造方法,如果子類有多個直接父類,那麼排在前面的父類的構造方法會被優先使用。例如如下代碼:

示例代碼: super:py

class Employee :
    def __init__ (self, salary):
        self.salary = salary
    def work (self):
        print('普通員工正在寫代碼,工資是:', self.salary)
class Customer:
    def __init__ (self, favorite, address):
        self.favorite = favorite
        self.address = address
    def info (self):
        print(f'我是一個顧客,我的愛好是: {self.favorite},地址是{self.address}' )
class Manager1 (Employee,Customer):
    pass
class Manager2 (Customer, Employee):
    pass
m1 = Manager1(1235)
m1.work()
m2 = Manager2('服務器', '北京')
m2.info() 

上面程序中定義了 Manager1類,該類繼承了 Employee和 Customer兩個父類。接下來程序中的Manager類將會優先使用 Employee類的構造方法(因爲它排在前面),所以程序使用Manager(1235)來創建 Manager1對象。該構造方法只會初始化 salary實例變量,因此執行上面程序是沒有任何問題的。但如果爲Manager2傳遞一個數值就會引發錯誤,因爲Manager2使用了Customer的構造方法,因此應該使用Manager2(‘服務器’, ‘北京’)創建Manager2對象。

爲了讓 Manager能同時初始化兩個父類中的實例變量,Manager應該定義自己的構造方法就是重寫父類的構造方法。 Python要求:如果子類重寫了父類的構造方法,那麼子類的構造方法必須調用父類的構造方法。子類的構造方法調用父類的構造方法有兩種方式。

使用未綁定方法,這種方式很容易理解。因爲構造方法也是實例方法。當然可以通過這種方式來調用;
使用supe()函數調用父類的構造方法;
在交互式解釋器中輸入help(super)查看 super()函數的幫助信息,可以看到如下輸出信息。

class super(object)
   super() -> same as super(__class__, <first argument>)
   super(type) -> unbound super object
   super(type, obj) -> bound super object; requires isinstance(obj, type)
   super(type, type2) -> bound super object; requires issubclass(type2, type)
   Typical use to call a cooperative superclass method:
   class C(B):
       def meth(self, arg):
           super().meth(arg)
   This works for class methods too:
   class C(B):
       @classmethod
       def cmeth(cls, arg):
           super().cmeth(arg)
   
   Methods defined here:
   
   __get__(self, instance, owner, /)
       Return an attribute of instance, which is of type owner.
   
   __getattribute__(self, name, /)

從輸出的內容可以看出,super其實是一個類,因此調用 super()的本質就是調用 super類的構造方法來創建 super對象。

從上面的幫助信息可以看到,使用 super()構造方法最常用的做法就是不傳入任何參數(這種做法與 super(type,obj)的效果相同),然後通過 super對象的方法既可調用父類的實例方法,也可調用父類的類方法。在調用父類的實例方法時,程序會完成第1個參數self的自動綁定。

現在將上面的代碼改成下面的形式:

class Employee :
    def __init__ (self, salary):
        self.salary = salary
    def work (self):
        print('普通員工正在寫代碼,工資是:', self.salary)
class Customer:
    def __init__ (self, favorite, address):
        self.favorite = favorite
        self.address = address
    def info (self):
        print(f'我是一個顧客,我的愛好是: {self.favorite},地址是{self.address}')

# Manager繼承了Employee、Customer
class Manager(Employee, Customer):
    # 重寫父類的構造方法
    def __init__(self, salary, favorite, address):
        print('--Manager的構造方法--')
        # 通過super()函數調用父類的構造方法
        super().__init__(salary)
        # 使用未綁定方法調用父類的構造方法
        Customer.__init__(self, favorite, address)
# 創建Manager對象
m = Manager(25000, 'IT產品', '廣州')
m.work()
m.info()  

在這段代碼中兩行粗體字代碼分別示範了兩種方式調用父類的構造方法。通過這種方式,Manager類重寫了父類的構造方法,並在構造方法中顯式調用了父類的兩個構造方法執行初始化,這樣兩個父類中的實例變量都能被初始化。

運行上面程序,可以看到如下運行結果:

--Manager的構造方法--
普通員工正在寫代碼,工資是: 25000
我是一個顧客,我的愛好是: IT產品,地址是廣州

-----------------支持作者請轉發本文,也可以加李寧老師微信:unitymarvel,或掃描下面二維碼加微信--------
在這裏插入圖片描述

歡迎關注 極客起源 微信公衆號,更多精彩視頻和文章等着你哦!
在這裏插入圖片描述

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