-
這一節是講 OOP Inheritance 的序章,Python 支持一個以上的類別繼承,因此在類別物件的繼承上,可以是單一繼承或是多重繼承。而首先第六節要聚焦在單一繼承的概念上,之後的章節則會接著講述多重繼承的概念。
-
在進入課程之前,首先來快速導覽一下 Python 的 Inheritance 概念,以下的前導文章(也包含下圖)均取自 Types of inheritance Python – GeeksForGeeks
Inheritance 繼承:
Python 的繼承主要可分為五種方式:
- 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()
- 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()
-
Multilevel Inheritance 縱向式(多層式)繼承: 一個子類別繼承自父類別後,這個子類別又再作為父類別,使其他的子類別(孫)繼承該子類別,而形成階層式的繼承關係。
-
Hierarchical Inheritance 橫向式繼承: 一個父類別,同時由多個子類別繼承的橫向繼承關係。
-
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 Baptiste 的 Python Deep Dive VI 課程簡報內容)
- 由於委派時,會再呼叫子類別以外的父類別的方法或屬性,因此使用 super().xxx() 呼叫父類別的方法與屬性時,要特別注意程式碼的順序性,避免因為委派,讓子類別的屬性值被誤寫回父類別的屬性值,如以下截圖(from Dr. Fred Baptiste 的 Python 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 → slots,slots 類似 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 Baptiste 的 Python Deep Dive VI 課程簡報內容)
* 那節省了記憶體,會否會影響到效能? --> 結論是使用 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 有這麼多優點,為何不設成全時(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 上。
2. Slots 與 Singel Inheritance 間的交互影響:
-
讓我們來看看 Single Inheritance 與 slots 的交互影響,如下圖所示: (以下截圖取自Dr. Fred Baptiste 的 Python 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) 課程簡報內容)
* 另一種情形是當父類別沒有使用 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 的重點摘要 ~