Python Deep Dive IV 第 6 節 Single Inheritance

  • 這一節是講 OOP Inheritance 的序章,Python 支持一個以上的類別繼承,因此在類別物件的繼承上,可以是單一繼承或是多重繼承。而首先第六節要聚焦在單一繼承的概念上,之後的章節則會接著講述多重繼承的概念。

  • 在進入課程之前,首先來快速導覽一下 Python 的 Inheritance 概念,以下的前導文章(也包含下圖)均取自 Types of inheritance Python – GeeksForGeeks
    Image3

Inheritance 繼承:

Python 的繼承主要可分為五種方式:

  1. Single Inheritance 單一繼承: 衍生的子類別繼承自單一的父類別,例如:
# Python program to demonstrate
# single inheritance

# Base class
class Parent:
    def func1(self):
        print("This function is in parent class.")

# Derived class
class Child(Parent):
    def func2(self):
        print("This function is in child class.")

# Driver's code
object = Child()
object.func1()
object.func2()
  1. Multiple Inheritance 多重繼承: 衍生的子類別(Child Class),繼承自多重的父類別(Parents Class) ,例如:
# Python program to demonstrate
# multiple inheritance

# Base class1
class Mother:
    mothername = ""
    def mother(self):
        print(self.mothername)

# Base class2
class Father:
    fathername = ""
    def father(self):
        print(self.fathername)

# Derived class
class Son(Mother, Father):
    def parents(self):
        print("Father :", self.fathername)
        print("Mother :", self.mothername)

# Driver's code
s1 = Son()
s1.fathername = "RAM"
s1.mothername = "SITA"
s1.parents()
  1. Multilevel Inheritance 縱向式(多層式)繼承: 一個子類別繼承自父類別後,這個子類別又再作為父類別,使其他的子類別(孫)繼承該子類別,而形成階層式的繼承關係。

  2. Hierarchical Inheritance 橫向式繼承: 一個父類別,同時由多個子類別繼承的橫向繼承關係。

  3. Hybrid Inheritance 混合式繼承: 綜合以上幾種的混合繼承架構。
    (礙於篇幅的關係,就不貼範例了,其實剛剛擷取的範例,也都取自 Types of inheritance Python --GeeksForGeeks 這篇中的範例)

Single Inheritance:

什麼是繼承?

老師首先以形狀來對比類別的繼承概念,比方說形狀基本上可以區分為多邊形(Polygon)、線條(Line)以及橢圓形(Ellipse),而其中圓形(Circle)是橢圓形的特例,而四邊形(Quadrilateral)是多邊形的一個特例,而矩形(Rectangle)又是四邊形的特例,再以下正方形(Square)又是矩形的特例,這樣從正方形一路到矩形、四邊形、多邊形,形成了一個特殊的繼承鏈。

同理,三角形(Triangle)也是多邊形的一個特例,而等腰三角形(Isosceles)則是三角形的一個特例,再往下正三角形(Equilateral)又是等腰三角形的特例,如下圖,諸如此類,這樣的彼此間的從屬關係,最終形成一個 IS-A Relationships,這也是 OOP 中用來表示 a 包含 b 的包含關聯圖(subsumption relationship,細節可參考 維基百科的說明 )。

IS-A Relationships 能夠充分表達抽象類別之間的超類(Super; Parents)與子類(Child)的關係,而透過繼承,子類會因繼承(Inherit)而擁有超類的一些屬性與方法,而子類除了超類的屬性與方法外,能進一步延伸(Extend)產生自己特有的屬性與方法,例如多邊形的邊長、內角等等,更有甚者,能夠複寫(Override)超類的方法或屬性,例如三角形的邊長~


(上圖擷取自 Udemy Dr. Fred Baptiste Python Deep Dive IV 課程內容)

Single Inheritance 單一繼承

Single Inheritance 單一繼承: 衍生的子類別繼承自單一的父類別,這也是上與下最單純的繼承關係:

  • 父類別中的屬性或方法,可以在子類別的 Instance 產生後就自然能使用這些屬性或方法~

  • 子類別可以透過複寫(Override)來改變原先從繼承的父類別過來的屬性或函式

  • 子類別中可以自行創建自己的屬性或方法(非繼承自父類別的屬性或方法),稱為延伸(Extend)

  • 子類別的 Instance,自然由於子類別的繼承關係,也會是父類別的 Instance

  • 即便是最底層的父類別,也仍存在繼承關係,所有的類別都繼承自 Python object class,因此所有的類別也都不需經由複寫 Dunder Method,而能擁有一些 Dunder Method 的屬性,例如 __name__, __class__, __init__, etc.

  • 在繼承鏈上的任何父類別中的方法,我們都可以在子類別中進行複寫,甚至包含最底層的 object class

    • 當對一個類別使用 str() 來顯示這個類別的名稱時,如果是子類別,且如果 __repr__ 沒有被覆寫,其實返回的會是父類別的類別名稱,但這樣的話,這個子類別的實例物件,使用 str() 顯示的是父類別的類別名稱,那這樣不是很怪嗎?

    • 技巧提示: 如果我們希望每個類別(不論是父類別或子類別)的實例化物件在使用 str() 都各自回應其實例化(直接)來源類別的名稱時,剛如何做呢?

    • 一般的方法,是在每個子類別中,各自複寫其 __repr__ 方法,重新定義各自要返回的字串內容,以此來修正上述這樣的問題

    • 但其實有另一個方法,可以一次性地修改來解決,以避免要繁瑣的在每個子類別中來複寫 __repr__ 這個方法:

      • 首先我們知道物件有一個 __class__ 的 callable 可以返回這個物件被創建的來源類別

      • 此外類別中有一個 __name__ 可以返回一個包含這個類別名稱的字串

      • 因此上述兩者合併之後,我們可以透過 obj.__class__.__name__ 取得創建這個實例物件的來源類別(self)的類別名稱字串,當我們在父類別中定義 __repr__ 以此種方式來返回類別名稱時,在子類別與父類別就會各自返回其父與子類別各自的類別名稱~


(上圖擷取自 Udemy Dr. Fred Baptiste Python Deep Dive IV 課程內容)

如何驗證兩個類別彼此存在繼承關係?

  • issubclass(child-c, parent-c): 只能用於驗證兩類別的從屬繼承關係,第一個參數必須是子類別,第二個參數則是父類別,此方法不直接適用於 Instance 的驗證,如要用於 Instance 驗證 → issubclass(type(obj), parent-c)

    注意: 如果 a 是 b 的子類別,那麼 issubclass(a, b) → True;但是 issubclass(b, a) → False,換言之,如果子類別不放在前面的話,即便這兩個類別有繼承關係,返回的結果也是否定的~

  • isinstance(obj, class): 此方法是我們最常使用的,其中 obj 參數是 instance,class 參數則是要驗證是否有從屬關係的目標類別,由於從屬關係指的是整個繼承鏈,因此這個 class 可以是直接 instance 的來源子類別,也可以是這個子類別於繼承鏈上的所有父類別~

  • 如果你想用 isinstance() 來驗證的兩個類別間的繼承關係,可以先將其中一個類別進行 Instance 創建,再由創建的 Instance 來使用 isinstance() 驗證與目標類別有無從屬關係

  • 在條件式中使用 type() == instance-class 與 isinstance(),老師比較推薦使用 isinstance() 來作為條件,因為 type() 無法驗證這個子類物件與超類的從屬關係,特別是如果 Instance 是來自於一個繼承的子類別時,會很容易造成條件誤判~

範例演示 for Single Inheritance

class Shape:
    pass

class Ellipse(Shape):
    pass

class Circle(Ellipse):
    pass

class Polygon(Shape):
    pass

class Rectangle(Polygon):
    pass

class Square(Rectangle):
    pass

class Triangle(Polygon):
    pass


issubclass(Ellipse, Shape) # True
issubclass(Square, Shape) # True
e = Ellipse()
isinstance(e, Ellipse) # True


class Shape:
    def __init__(self, name):
        self.name = name
        
    def info(self):
         return f'Shape.info called for Shape({self.name})'
    
    def extended_info(self):
        return f'Shape.extended_info called for Shape({self.name})'
    
class Polygon(Shape):
    def __init__(self, name):
        self.name = name  # we'll come back to this later in the context of using the super()
        
    def info(self):
        return f'Polygon info called for Polygon({self.name})'

p = Polygon('square')
p.info() # 'Polygon info called for Polygon(square)'
p.extended_info() # 'Shape.extended_info called for Shape(square)'


class Shape:
    def __init__(self, name):
        self.name = name
        
    def info(self):
         return f'Shape.info called for Shape({self.name})'
    
    def extended_info(self):
        return f'Shape.extended_info called for Shape({self.name})', self.info()
    
class Polygon(Shape):
    def __init__(self, name):
        self.name = name  # we'll come back to this later in the context of using the super()
        
    def info(self):
        return f'Polygon.info called for Polygon({self.name})'

p = Polygon('Square')
p.info() # 'Polygon.info called for Polygon(Square)'
print(p.extended_info()) # ('Shape.extended_info called fo,bjr Shape(Square)', 'Polygon.info called for Polygon(Square)')

單一繼承中的子類別的行為:

1. Overriding 複寫: 父類別中定義的屬性或方法,子類別可以在其類別中重新宣告及定義 → Overriding 複寫。

  • 如上面範例 Polygon 子類別繼承了 Shape 父類別,但在 Polygon 子類別中重新宣告了 info method,而此 info method 在 Shape 父類別中有定義過,因此以 info method 來說,Polygon 子類別複寫了 Shape 父類別的 info method。

2. Extending 延伸: 子類別繼承父類別後,可於子類別中定義父類別沒有的新的屬性或方法 → Extending 延伸。

  • 例如下面範例所示,Student 類別繼承了 Person 父類別,而 Student 類別中新定義了一個 study method,此 study method 是 父類別 Person 中所沒定義過的方法:
class Person:
    pass

class Student(Person):
    def study(self):
        return 'study... study... study...'

p = Person()
try:
    p.study()
except AttributeError as ex:
    print(ex)

Delegating to Parent (從父類別中)委派: 當子類別繼承父類別後,子類別不須宣告(如果宣告了,就變成是複寫) 就能呼叫父類別的屬性或方法來直接使用 → Delegating to Parent。

  • 當要進行對父類別屬性或方法的呼叫時,記得要使用關鍵字 super()(特別注意: 不是使用 self,self 指的是子類別自己)
  • 即便是委派(從父類別的方法或屬性)的行為,如果一個屬性或方法,在子類別與父類別同時存在,子類別的 Instance 會優先呼叫子類別上的同名屬性或方法,此為一種遮蔽效應,因此當一個單一且多重繼承的子類別,則會椅子類別為首,當進行委派呼叫時,會逐層從子類別找,再接續到父類別,而後才是祖父,曾祖父,曾曾祖父的逐層往上找,往上進行委派呼叫,如以下範例(截圖取自 Dr. Fred BaptistePython Deep Dive VI 課程簡報內容)

  • 由於委派時,會再呼叫子類別以外的父類別的方法或屬性,因此使用 super().xxx() 呼叫父類別的方法與屬性時,要特別注意程式碼的順序性,避免因為委派,讓子類別的屬性值被誤寫回父類別的屬性值,如以下截圖(from Dr. Fred BaptistePython Deep Dive VI 課程簡報內容)所示:

Slots 與 Single Inheritance:

1. Slots:

  • 我們知道 Instance 的 Attribute 都存放到本地端的 dict 集合中,因此對於 Instance 的產生會對本地端的 dict(存放在執行環境當下的 memory 中) 造成一些 overhead,因此當產生 Instance 的類別中的屬性或方法越多,以及 Instance 產生的越多時,對本地端環境的 memory 的 overhead 也就越大。
  • 因此從 Python 3.3 起,Python 引入了 key sharing dictionaries 來緩解這個問題,但有另一個方式更好,就是使用 slots → slotsslots 類似 dicts 也是一個寄存物件的字典物件!
class Location:
    __slots__ = 'name', '_longitude', '_latitude'
    
    def __init__(self, name, longitude, latitude):
        self._longitude = longitude
        self._latitude = latitude
        self.name = name
        
    @property
    def longitude(self):
        return self._longitude
    
    @property
    def latitude(self):
        return self._latitude
  • 我們可以在 Class 定義時,先使用 slots 來存放這些屬性,如下圖示,而當 Python Interpreter 在解析到這個類別時,就會知道這些屬性已被存放到 slots 中,而不會額外 Create dict 來存放這些已被存到 slot 中的屬性或方法
  • 我們可以在 Class 定義時,先使用 slots 來存放這些屬性,如下圖示,而當 Python Interpreter 在解析到這個類別時,就會知道這些屬性已被存放到 slots 中,而不會額外 Create dict 來存放這些已被存到 slot 中的屬性或方法
    • 真有適用於 slots 的場合嗎? 何時會大量產生 Instance,其實像從資料庫返回多筆(可能成百上千筆的資料筆數) 資料列,每個資料列舊都可看成是一個 Instance,像這樣的場景就很適用使用 slots 來節省記憶體。(以下截圖取自Dr. Fred BaptistePython Deep Dive VI 課程簡報內容)

Image3

*  那節省了記憶體,會否會影響到效能? --> 結論是使用 slots 甚至比一般字典還快~ (以下截圖取自[Dr. Fred Baptiste](https://www.udemy.com/user/fredbaptiste/) 的 [Python Deep Dive VI](https://www.udemy.com/course/python-3-deep-dive-part-4) 課程簡報內容)

Image4

* 既然 slots 有這麼多優點,為何不設成全時(All the time)都使用 slots?
    * 這是因為 slots 有一個不便性,當我們在一個 Class 上使用了 slots,那麼這個 Class create 的 Instance 都不能增加任何原先沒在 slots 中定義的屬性,如下圖所示~ (以下截圖取自[Dr. Fred Baptiste](https://www.udemy.com/user/fredbaptiste/) 的 [Python Deep Dive VI](https://www.udemy.com/course/python-3-deep-dive-part-4) 課程簡報內容)
        * 一旦使用了 slots,__dict__ 就會被移除,因此我們不再能自由地增加屬性到 Instance 上。

Image5

2. Slots 與 Singel Inheritance 間的交互影響:

  • 讓我們來看看 Single Inheritance 與 slots 的交互影響,如下圖所示: (以下截圖取自Dr. Fred BaptistePython Deep Dive VI 課程簡報內容)

    • 所以當一個子類別繼承的父類別有使用 slots,那麼這個子類別也會有父類別的 slots 部分,也能有它自己的 Instance Dictionary (For subclass’s own extension attribute)

* 那如果子類別也想要把 extension attribute 也寄存到 slots 裡,該如何做?
    * 子類別可以自己也在類別中指定 __slots__ 來存放自己的屬性!!! (以下截圖取自[Dr. Fred Baptiste](https://www.udemy.com/user/fredbaptiste/) 的 [Python Deep Dive VI](https://www.udemy.com/course/python-3-deep-dive-part-4) 課程簡報內容)

* 要注意一點,當子類別也指派了新的屬性存放到 slots 中,但新屬性名稱又與父類別存放到 slots 的屬性名稱一樣時,會發生什麼,以目前而言,仍能正常運作,但會增加記憶體的消耗,但老師也提到,未來的話,在 Python 官方文件有提到,未來會加入檢查機制來防止這樣的狀況發生(說不定就直接 raise an exception error)
* 當剛剛那樣的情形(當子類別也指派了新的屬性存放到 slots 中,但新屬性名稱又與父類別存放到 slots 的屬性名稱一樣時)發生時會如何?
    * 除了子類別同名屬性會遮蔽父類別同名屬性外,如果原來父類別同名屬性有設定檢查機制(但子類別上沒設定),那這些設定也會無效(直接 follow 子類別屬性的定義,如父類別有定義,子類別上沒定義,則就會被蓋掉而變成沒定義),如以下截圖所示(from [Dr. Fred Baptiste](https://www.udemy.com/user/fredbaptiste/) 的 [Python Deep Dive VI](https://www.udemy.com/course/python-3-deep-dive-part-4) 課程簡報內容)

Image8

* 另一種情形是當父類別沒有使用 slots,但子類別有使用 slots 時,會如何?
    * 子類別仍能正常使用 slots 不會報錯,但是子類別衍生的 Instance,這些寄存到 slots 中的屬性,仍會出現在本地端的 __dict__ 裡~
class Person:
    def __init__(self, name):
        self.name = name
        
class Student(Person):
    __slots__ = 'age', 
    
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

s = Student('Python', 30)
s.name, s.age, s.__dict__ # ('Python', 30, {'name': 'Python'})
* 接下來來探討被寄存到 slots 裡的屬性(slotted attributes) 與原先的屬性(Properties)(像 getter、setter 這些屬性)有沒有不一樣,如有不同,不同點是什麼?
    * 最直接的不同,一個已被寄存到 slots 中的屬性,並不會再被儲存到 Instance 的 __dict__ 中
    * 像 getter、setter 這些 @properties 屬性設置,本來也都沒存在 Instance 的 __dict__ 中
    * 本質上,這兩者 (slots & @properties) 並無不同,都是出自 data descriptors 這個類別
    * slots 中的屬性執行比一般屬性(Attributes)快且更少的儲存需求(前面已經提過了)
    * 但 Instance dictionary 則賦於屬性(Attributes)的指派與設定有更多的自由度
    * 那我們兩者可同時使用嗎? 
        * 答案是肯定的,我們可以把 __dict__ 直接指派寄存到 __slots__ 中:
class Person:
    __slots__ = 'name', '__dict__'
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person('Alex', 19)
p.name, p.age, p.__dict__ # ('Alex', 19, {'age': 19})
p.school = 'Berkeley'
p.__dict__ # {'age': 19, 'school': 'Berkeley'}

~ 以上大致是第六節 Single Inheritance 的重點摘要 ~

1個讚