繼承和多型


繼承和多型 - 這是Python中一個非常重要的概念。我們必須更好地理解它。

繼承

物件導向程式設計的一個主要優勢是重用。 繼承是實現這一目標的機制之一。 繼承允許程式員先建立一個通用類或基礎類別,然後再將其擴充套件為更專門化的類。 它允許程式員編寫更好的程式碼。

使用繼承,可以使用或繼承基礎類別中可用的所有資料欄位和方法。 之後,可以新增自己的方法和資料欄位,因此繼承提供了一種組織程式碼的方法,而不是從頭開始重寫。

在物件導向的術語中,當類X擴充套件類Y時,則Y稱為超級/父/基礎類別,X稱為子類/子/派生類。 這裡需要注意的一點是,只有資料欄位和非專用的方法才能被子類存取。 私有資料欄位和方法只能在類中存取。

建立派生類的語法是 -

class BaseClass:
   Body of base class
class DerivedClass(BaseClass):
   Body of derived class

繼承屬性

現在看下面的程式碼例子 -


class Date(object):
    def get_date(self):
        return "2018-06-30"

class Time(Date):
    def get_time(self):
        return "09:09:09"

dt = Date()
print("Get date from Date class: ", dt.get_date())

tm = Time()
print("Get time from Time class: ", tm.get_time())
print("Get date from class by inheriting or calling Date class method: ", tm.get_date())

執行上面範例程式碼,得到以下結果 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
Get date from Date class:  2018-06-30
Get time from Time class:  09:09:09
Get date from class by inheriting or calling Date class method:  2018-06-30

首先建立了一個名為Date的類,並將該物件作為引數傳遞,object是由Python提供的內建類。 之後建立了另一個名為time的類,並將Date類稱為引數。 通過這個呼叫,可以存取Date類中的所有資料和屬性。 正因為如此,建立的Time類物件tm中獲取父類別中get_date方法。

Object.Attribute查詢層次結構

  • 範例
  • 當前類
  • 該類繼承的任何父類別

繼承範例

讓我們來看看一個繼承的例子 -

讓我們建立幾個類來參與範例 -

  • Animal - 模擬動物的類
  • Cat - Animal的子類
  • Dog - Animal的子類

在Python中,類別建構函式用於建立物件(範例),並為屬性賦值。

子類別建構函式總是呼叫父類別的建構函式來初始化父類別中的屬性的值,然後它開始為其屬性賦值。


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

    def eat(self, food):
        print('%s is eating %s , '%(self.name, food))

class Dog(Animal):

    def fetch(self, thing):
        print('%s goes after the %s !'%(self.name, thing))


class Cat(Animal):

    def swatstring(self):
        print('%s shreds the string! ' % (self.name))


d = Dog('Ranger')
c = Cat("Meow")

d.fetch("ball");
c.swatstring()
d.eat("Dog food")
c.eat("Cat food")
## 呼叫一個沒有的定義的方法
#d.swatstring()

執行上面範例,得到以下程式碼 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
Ranger goes after the ball !
Meow shreds the string! 
Ranger is eating Dog food , 
Meow is eating Cat food ,

在上面的例子中,我們看到如何父類別中的屬性或方法,以便所有的子類或子類都會從父類別繼承那些屬性。

如果一個子類嘗試從另一個子類繼承方法或資料,那麼它將通過一個錯誤,就像看到當Dog類嘗試呼叫cat類有swatstring()方法時,它會丟擲一個錯誤(AttributeError)。

多型性(「多種形狀」)

多型性是Python中類定義的一個重要特性,當您在類或子類中使用通用命名方法時,可以使用它。 這允許功能在不同時間使用不同型別的實體。 所以,它提供了靈活性和鬆散耦合,以便程式碼可以隨著時間的推移而擴充套件和輕鬆維護。

這允許函式使用任何這些多型類的物件,而不需要知道跨類的區別。

多型性可以通過繼承進行,子類使用基礎類別方法或覆蓋它們。

我們用之前的繼承範例理解多型的概念,並在兩個子類中新增一個名為show_affection的常用方法 -

從這個例子可以看到,它指的是一種設計,其中不同型別的物件可以以相同的方式處理,或者更具體地說,兩個或更多的類使用相同的名稱或通用介面,因為同樣的方法(下面的範例中的show_affection) 用任何一種型別的物件呼叫。


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

    def eat(self, food):
        print('%s is eating %s , '%(self.name, food))

class Dog(Animal):

    def fetch(self, thing):
        print("{0} wags {1}".format(self.name, thing))

    def show_affection(self):
        print("{0} wags tail ".format(self.name))

class Cat(Animal):

    def swatstring(self):
        print('%s shreds the string! ' % (self.name))

    def show_affection(self):
        print("{0} purrs ".format(self.name))

d = Dog('Ranger')
c = Cat("Meow")

d.show_affection()
c.show_affection()

執行上面範例程式碼,得到以下結果 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
Ranger wags tail 
Meow purrs

所以,所有的動物都表現出喜愛(show_affection),都不太相同。 「show_affection」行為因此具有多型性,因為它根據動物的不同而採取不同的行為。 因此,抽象的「動物」概念實際上並不是「show_affection」,而是特定的動物(如狗和貓)具有動作「show_affection」的具體實現。

Python本身具有多型的類。 例如,len()函式可以與多個物件一起使用,並且都會根據輸入引數返回正確的輸出。

過載

在Python中,當子類包含一個覆蓋超類方法的方法時,也可以通過呼叫超類方法 -

Super(Subclass, self).method而不是self.method

範例

class Thought(object):
   def __init__(self):
      pass
   def message(self):
      print("Thought, always come and go")

class Advice(Thought):
   def __init__(self):
      super(Advice, self).__init__()
   def message(self):
      print('Warning: Risk is always involved when you are dealing with market!')

繼承建構函式

從前面的繼承範例中看到,__init__位於父類別中,因為子類-DogCat沒有__init__方法。 Python使用繼承屬性查詢來查詢動物類中的__init__。 當我們建立子類時,首先它會查詢dog類中的__init__方法,如果它找不到它,則查詢父類別Animal,並在那裡找到並在那裡呼叫它。 因此,當類設計變得複雜時,可能希望初始化一個範例,首先通過父類別建構函式然後通過子類建構函式處理它。參考以下範例程式碼 -


import random

class Animal(object):

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

class Dog(Animal):

    def __init__(self, name):
        super(Dog, self).__init__(name)
        self.breed = random.choice(['Doberman', 'German shepherd', 'Beagle'])

    def fetch(self, thing):
        print('%s goes after the %s !'%(self.name, thing))

d = Dog('黑皮')
print(d.name)
print(d.breed)

執行上面範例程式碼,得到以下結果 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
黑皮
German shepherd

在上面的例子中,所有的動物都有一個名字,所有的狗都是一個特定的品種。我們用super來呼叫父類別建構函式。所以Dog類有它自己的__init__方法,但首先呼叫的我們稱之為超類。 Super是在函式中構建的,它是用來將一個類與它的超類或它的父類別關聯起來。

在這種情況下,我們說獲取超類-Dog並將它範例傳遞給建構函式 - __init__。換句話說,我們用dog物件呼叫父類別Animal.__init__方法。你可能會問,為什麼不會只用Animal.__init__()Dog的範例,我們可以做到這一點,但如果Animal類的名字會在將來某個時候改變。如果想重新安排類層次結構,那麼該Dog會從另一個類繼承。在這種情況下使用super可以讓保持模組化,易於更改和維護。

所以在這個例子中,能夠將通用__init__功能與更具體的功能相結合。這使有機會將通用功能與特定功能分開,從而消除程式碼重複,並以反映系統總體設計的方式將類相互關聯。

結論

  • __init__與任何其他方法一樣; 它可以被繼承
  • 如果一個類沒有__init__建構函式,Python將檢查其父類別查詢。
  • 只要找到有一個__init__建構函式,Python就會呼叫它並停止查詢
  • 可以使用super()函式來呼叫父類別中的方法。
  • 可能想要在父類別以及子類進行初始化。

多重繼承和查詢樹

正如其名稱所示,Python的多重繼承是當一個類從多個類繼承時。

例如,一個孩子繼承父母(母親和父親)的個性特徵。

Python多繼承語法

要使一個類繼承多個父類別,可將這些類的名稱寫在派生類的括號內,同時定義它。 我們用逗號分隔這些名字。

下面是一個例子 -

>>> class Mother:
   pass

>>> class Father:
   pass

>>> class Child(Mother, Father):
   pass

>>> issubclass(Child, Mother) and issubclass(Child, Father)
True

多繼承是指從兩個或兩個以上的類繼承的能力。 當孩子從父母繼承而父母從祖父母類繼承時,複雜性就出現了。 Python在繼承樹上尋找正在被請求從物件讀取的屬性。 它將檢查範例,在類中然後在父類別中檢查,最後從祖父類別中檢查。 現在問題出現在按什麼順序搜尋類 - 廣度優先或深度優先。 預設情況下,Python採用深度優先。

這就是為什麼在下圖中Python首先在A類中搜尋dothis()方法。所以下面例子中的方法解析順序將是 -

Mro- D→B→A→C

看下面的多重繼承圖 -

通過一個例子來理解Python的「mro」特性。


class A(object):

    def dothis(self):
        print('doing this in A')

class B(A):
    pass

class C(object):
    def dothis(self):
        print('doing this in C')

class D(B, C):
    pass

d_inst =  D()
d_inst.dothis()
print(D.mro())

執行上面範例程式碼,得到以下結果 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
doing this in A
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.C'>, <class 'object'>]

範例3

下面再來看另一個「菱形」多重繼承的例子。

上圖將被視為含糊不清。 從上之前的例子中了解「方法解析順序」。 mro將是D→B→A→C→A,但事實並非如此。 在從C獲得第二個A時,Python將忽略之前的A。因此在這種情況下mro將是D→B→C→A

我們來根據上面的圖建立一個例子 -


class A(object):

    def dothis(self):
        print('doing this in A')

class B(A):
    pass

class C(A):
    def dothis(self):
        print('doing this in C')

class D(B, C):
    pass

d_inst =  D()
d_inst.dothis()
print(D.mro())

執行上面範例程式碼,得到以下結果 -

E:\worksp\pycharm\venv\Scripts\python.exe E:/worksp/pycharm/main.py
doing this in C
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

理解上述輸出的簡單規則是 - 如果在方法解析度順序中出現相同的類,則將從方法解析度順序中刪除此類的早期外觀。

總結如下 -

  • 任何類都可以從多個類繼承
  • 搜尋繼承類時,Python通常使用「深度優先」順序。
  • 但是,當兩個類從同一個類繼承時,Python將從該mro中消除該類的第一次出現。

裝飾器,靜態和類方法

函式(或方法)由def語句建立。

雖然方法的工作方式與函式完全相同,除了方法第一個引數是範例物件的一點。

我們可以根據行為方式來分類方法

  • 簡單的方法 - 在類的外部定義。 該函式可以通過提供範例引數來存取類屬性:
    def outside_func(():
    
  • 範例方法 -
    def func(self,)
    
  • 類方法 - 如果需要使用類屬性
    @classmethod
    def cfunc(cls,)
    
  • 靜態方法 - 沒有關於該類的任何資訊
    @staticmethod
    def sfoo()
    
    到目前為止,我們已經看到了範例方法,下面來了解其他兩種方法。

1. 類方法

@classmethod裝飾器是一個內建的函式裝飾器,它通過呼叫它的類或作為第一個引數呼叫的範例的類。 評估結果會影響函式定義。

語法

class C(object):
   @classmethod
   def fun(cls, arg1, arg2, ...):
      ....
fun: function that needs to be converted into a class method
returns: a class method for function

他們有權存取此cls引數,它不能修改物件範例狀態。

  • 它受到類的約束,而不是類的物件。
  • 類方法仍然可以修改適用於類的所有範例的類狀態。

2. 靜態方法

靜態方法既不接受自己也不接受cls(class)引數,但可以自由接受任意數量的其他引數。

語法

class C(object):
   @staticmethod
   def fun(arg1, arg2, ...):
   ...
returns: a static method for function funself.
  • 靜態方法既不能修改物件狀態,也不能修改類的狀態。
  • 受限於可以存取的資料。

什麼時候使用

  • 通常使用類方法來建立工廠方法。 工廠方法返回不同用例的類物件(類似於建構函式)。
  • 通常使用靜態方法來建立實用函式。