今天起介紹物件導向程式的第二大支柱:繼承(Inheritance)。不過在正式開始前,先補充一下昨天的封裝本來要講卻漏掉的部分:
以下是筆者個人看法:
Hardwood
和Conifer
兩個類別(註2),讓這兩個新增類別都歸屬於Tree
。用物件導向語言來講,就是Hardwood
和Conifer
兩個類別都繼承自Tree
。Tree
通常稱為「父類別」(parent class, base class, super class),而Hardwood
和Conifer
則是「子類別」(child class, derived class, sub class)。class Tree():
__count = 0
def __init__(self, breed: str, age: int, height: int): # constructor
self.__breed = breed
self.__age = age
self.__height = height
Tree.__count += 1
@classmethod
@property
def count(cls) -> int:
'''The __count property(getter).'''
return cls.__count
@property
def breed(self) -> str:
'''The breed property(getter).'''
return self.__breed
@breed.setter
def breed(self, breed: str):
'''The breed property(setter).'''
if not isinstance(breed, str):
raise TypeError('樹種必須是字串。')
breeds = {'cedar': (0, 5_000), 'oak': (0, 300),
'beech': (0, 400), 'camphor': (0, 800),
'maple': (0, 500), 'phoebe': (0, 2_000)}
breed = breed.strip().lower()
if breed not in breeds:
raise Exception(f"樹種名稱'{breed}'不正確。")
min_age = breeds.get(breed)[0]
max_age = breeds.get(breed)[1]
if self.__age < min_age or self.__age > max_age:
raise Exception(f"新樹種名稱'{breed}'和其年齡不匹配。")
self.__breed = breed
@property
def age(self) -> int:
'''The age property(getter).'''
return self.__age
@age.setter
def age(self, age: int):
'''The age property(setter).'''
if isinstance(age, bool) or not isinstance(age, int):
raise TypeError('樹齡必須是整數。')
# 以下的條件判斷只是「示意」,實際上該和breed一併考慮才對。
if age > 15_000 or age < 0:
raise Exception(f'樹齡數字{age}不合理。')
self.__age = age
@property
def height(self) -> int:
'''The height property(getter).'''
return self.__height
@height.setter
def height(self, height: int):
'''The height property(setter).'''
if isinstance(height, bool) or not isinstance(height, int):
raise TypeError('樹高必須是整數。')
if height > 200 or height < 1:
raise Exception(f'樹高數字{height}不合理。')
self.__height = height
def show_info(self):
print(f'{self.breed=:10}{self._age=:<10,}{self.height=:<10}')
class Hardwood(Tree): # 繼承自Tree類別(註3)。
... # 暫不實作。
class Conifer(Tree): # 繼承自Tree類別。
... # 暫不實作。
def show_tree_info(breed: str, age: int, height: int):
print(f'{breed=:12}{age=:<8,}{height=:<10,}')
try:
tree1 = Hardwood('maple', 60, 30)
tree2 = Conifer('cedar', 1_500, 96)
show_tree_info(tree1.breed, tree1.age, tree1.height)
show_tree_info(tree2.breed, tree2.age, tree2.height)
tree1.breed = 'camphor' # 'camphor'是合法樹種名稱,應該賦值成功。
except Exception as e:
print(str(e))
finally:
print()
show_tree_info(tree1.breed, tree1.age, tree1.height)
def show_tree_info(breed: str, age: int, height: int):
print(f'{breed=:12}{age=:<8,}{height=:<10,}')
try:
tree1 = Hardwood('maple', 60, 30)
tree2 = Conifer('cedar', 1_500, 96)
show_tree_info(tree1.breed, tree1.age, tree1.height)
show_tree_info(tree2.breed, tree2.age, tree2.height)
tree1.breed = 'breadfruit' # 'breadfruit'是不合法的樹種名稱。
except Exception as e:
print(str(e))
finally:
print()
show_tree_info(tree1.breed, tree1.age, tree1.height)
breed
, age
和height
,原因是子類別透過繼承機制,取得了其父類別的「遺產」(註4)。這裡的遺產就是屬性(attributes and properties)和方法。class Hardwood(Tree):
# 這次子類別擁有自己的constructor了。
def __init__(self, breed: str, age: int, height: int, defoliation: dict, puberty: dict): # defoliation是落葉,puberty是開花期。
super().__init__(breed, age, height) # 呼叫父類別的constructor。
self.defoliation = defoliation # 子類別自己的屬性,暫設為public。
self.puberty = puberty # 子類別自己的屬性,暫設為public。
class Conifer(Tree):
def __init__(self, breed: str, age: int, height: int, price: int):
super().__init__(breed, age, height) # 呼叫父類別的constructor。
self.price = price # 子類別自己的屬性,暫設為public。
def sell(self, seller: str, buyer) -> bool: # 子類別自己的方法。
... # 實作略
print(f'{seller=:20}{buyer=:20}')
return True
super().__init__(breed, age, height)
這行來手動呼叫父類別的建構子,super()
代表父類別。經過手動呼叫,父類別的建構子就可以為子類別「重用」(reuse)。try:
tree1 = Hardwood('maple', 60, 30, {'start': 'Oct', 'end': 'Feb'}, {'start': 'May', 'end': 'Jul'})
tree2 = Conifer('cedar', 1_500, 96, 2_500)
print(f"{tree1.breed=:12}{tree1.age=:<8}{tree1.height=:<10,}\n{tree1.defoliation['start']=:8}{tree1.defoliation['end']=:8}\n{tree1.puberty['start'] =:8}{tree1.puberty['end'] =:8}")
print()
print(f"{tree2.breed=:12}{tree2.age=:<8}{tree2.height=:<10,}\n{tree2.price=:<10}")
print('呼叫tree2.sell():')
tree2.sell('Alex', 'Mirror')
except Exception as e:
print(str(e))
Tree
的資源和商業邏輯,同時也建立了自己的小家庭。註1: 本系列文章目的是物件導向程式設計介紹,並非植物學研究。像闊葉樹和針葉樹實際的不同特性,當然不會像本文說的那麼絕對。請讀者不要在這方面挑筆者的骨頭。
註2: 按PEP 8規範,類別的命名建議使用Pascal Case方式(詳見本系列Day 4文),所以是Hardwood
和Conifer
而非hardwood
和conifer
。另外,hardwood是單字,不能作HardWood
。
註3: 無繼承時,類別名稱後可不加小括號()
。有繼承時則一定要有()
,裡面的參數是其父類別。
筆者習慣即使是無繼承,類別名稱的後面也加上()
,以保持和有繼承時的語法一致,也和函數語法一致。函數沒有參數時不也要加小括號?
註4: 「遺產」一詞只是筆者一時興起的比喻,實際上子類別可是「椿萱並茂」。