34 Encapsulation, Inheritacne, and Polymorphism

  • 這一章要介紹

34.1 封裝

  • 把資料(attribute)和使用資料的方法(methods)全包在一起的作法,就叫封裝

34.2 繼承(inheritance)

  • 繼承的意思是,我們在define一個class的時候,可以繼承另一個class的所有attribue和methods
  • parent class指的是給別人繼承的class,又稱為base class
  • child class指的是繼承別人的class,又稱為deived class

34.2.1 simple inheritance

  • 例如,我先寫一個class叫Employee_parent,如下:
class Employee_parent:
  # class-level attribute
  MIN_SALARY = 30000
  # instance-level attribute
  def __init__(self, name, salary):
    self.name = name
    self.salary = salary
  # instance-level method
  def give_raise(self, amount):
    self.salary += amount
  • 這個class有兩個attribute,和一個method
  • 那如果今天我想寫一個WNC_Employee,他想繼承Employee_parent的這些attribute & methods,那就這樣寫:
class WNC_Employee(Employee_parent): #把要繼承的Class name塞入
  pass
  • 現在我們可以照往常輸入name和salary來initialize一個WNC_Employee的instance:
Hank_wnc = WNC_Employee("Hank", 22000)
  • 檢查一下之前的attribute都繼承下來了:
print(Hank_wnc.name)
#> Hank
print(Hank_wnc.salary)
#> 22000
  • 檢查一下method也繼承下來了:
Hank_wnc.give_raise(10000)
print(Hank_wnc.salary)
#> 32000

34.2.2 增加instance-level attribute & method

  • 那接下來,就來講如何加入我想新寫的attribute和method
  • 例如,對WNC_Employee來說,attribute除了name, salary外,還要新增一個職等(level); method除了原本的give_raise()外,還要新增一個promote()來更新職等的變化:
class Employee_parent:
  MIN_SALARY = 30000
  def __init__(self, name, salary):
    self.name = name
    self.salary = salary
  
  def give_raise(self, amount):
    self.salary += amount

class WNC_Employee(Employee_parent): #把要繼承的Class name塞入
  # 到目前為止,原本Employee有的東西,WNC_Employee都有了
  def __init__(self, name, salary, level = 3): # 要多加的level寫在這
    # call之前Emplolyee的init來用,所以寫了這步,
    # 等於是少寫self.name, self.salary = name, salary 這一行
    Employee_parent.__init__(self, name, salary)  
    self.level = level
    
  def promote(self, num_level):
    self.level = self.level + num_level
  • 看一下__init__那邊,一開始的init是為了initialize這個新的class的instance用的,而因為他繼承了Employee,所以原本描述Employee的所有attribute都要放入,所以一樣填入”name”和”salary”,而這個新的Class還要有新的attribute,叫”level”,用來描述職等。而這邊的self指的是WNC_Employee的instance的代名詞。
  • 那第二行出現的Employee_parent.__init__(self, name, salary),指的是我要套用之前Employee這個class的__init__函數,第一個self指的還是WNC_Employee的instance,但因為前面講過,他同時也是Employee的instance,所以可以這樣套進來。然後後面的name和salary就是用第一點傳入的name和salary。那這樣套用function後,他就會執行self.name = name, self.salary = salary 這兩行,所以,我只剩下self.level = level這行自己寫下來就好
  • 這邊提醒一下,沒有規定一定要用Employee_parent.__init__(),你想要累累的寫self.name = name; self.salary = salary也可以。上面的寫法只是讓你省力,所以大家都這樣寫而已。
  • 我們現在initialize一個instance看看:
Hank_wnc2 = WNC_Employee("Hank", 22000, 3)
print(Hank_wnc2.name)
#> Hank
print(Hank_wnc2.salary)
#> 22000
print(Hank_wnc2.level)
#> 3
  • 用新的function看看
Hank_wnc2.promote(1)
print(Hank_wnc2.level)
#> 4
  • 看起來很不錯,但如果我原本parent class的attribute就一大堆的話,我不就又要copy一堆attribute到我的__init__()裡面?其實不用,我們可以用*args和**kwargs直接代稱掉原本的arguments,例如以下這種寫法
class WNC_Employee2(Employee_parent): #把要繼承的Class name塞入
  # 到目前為止,原本Employee有的東西,WNC_Employee都有了
  def __init__(self, level, *args, **kwargs): # 要多加的level寫在這
    Employee_parent.__init__(self, *args, **kwargs) 
    self.level = level
    
  def promote(self, num_level):
    self.level = self.level + num_level

wnc_hank2 = WNC_Employee2(name = "Hank", salary = 22000, level = 3)
print(wnc_hank2.level)
#> 3
wnc_hank2.promote(1)
print(wnc_hank2.level)
#> 4

34.2.3 增加class-level attribute

  • 這其實很好理解,你都繼承原本的class了,所以包括原本的class level attribute, instance attribute, methods全都繼承下來
  • 所以,可以看一下剛剛的WNC_Employee的class level attribute: MIN_SALARY在不在
WNC_Employee.MIN_SALARY
#> 30000
  • 在嘛,合理。那這一節的重點,是放在我如果修改child class的class-level attribute,parent class的class-level attribute會不會跟著變?
  • 直接講結論:class level attribute可以繼承,而child class的class level attribute可以overwritten掉原本parent class的class level attribute,但不會改變parent class的class level attribute
class WNC_Employee_trial(Employee_parent):
  MIN_SALARY = 0
  pass

child = WNC_Employee_trial("Hank", 22000)
parent = Employee_parent("Hank", 22000)

print(child.MIN_SALARY)
#> 0
print(parent.MIN_SALARY)
#> 30000
print(WNC_Employee_trial.MIN_SALARY)
#> 0
print(Employee_parent.MIN_SALARY)
#> 30000

34.3 多形

  • 多形的意思是,子類別雖然繼承父類別的methods,但我可以命名一個名稱一模一樣的method,讓他over-ride原先的method,這樣就達到名稱一樣,但功能不一樣的現象,我們叫”多形”
  • WNC_Employee這個class為例,他繼承Employee_parent,所以也繼承了give_raise()這個method
  • 那如果WNC這間公司是個佛心公司,他的加薪方式,跟一般公司不同(你輸入要加的薪水後,我還會給你額外的加成福利),那我們有兩種做法:
    • 寫一個新的method叫give_raise_new()
    • 維持舊名稱give_raise(),但內容被我置換成wnc版本 -> 建議這麼做,而這麼做的結果就是”多形”
  • 直接看例子:
class Employee_parent:
  MIN_SALARY = 30000
  def __init__(self, name, salary):
    self.name = name
    self.salary = salary
  
  def give_raise(self, amount):
    self.salary += amount
    
class WNC_Employee3(Employee_parent):
  def __init__(self, level, *args, **kwargs):
    Employee_parent.__init__(self, *args, **kwargs) 
    self.level = level
      
  # 這邊開始修改新method
  def give_raise(self, amount, bonus=1): #method名稱同,但多個參數
    new_amount = amount * bonus
    Employee_parent.give_raise(self, new_amount) #使用parent class的method

Hank_in_parent = Employee_parent(name = "Hank", salary = 22000)
Hank_in_wnc = WNC_Employee3(name = "Hank", salary = 22000, level = 3)

Hank_in_parent.give_raise(amount=10000)
Hank_in_wnc.give_raise(amount=10000, bonus=2)

print(Hank_in_parent.salary)
#> 32000
print(Hank_in_wnc.salary)
#> 42000
class Animal:
  def eat(self):
    return("Animal is eating")
  
class Dog:
  def eat(self):
    return("Dog is eating")
  
class Pig:
  def eat(self):
    return("Pig is eating")

d = Dog()
p = Pig()

print(d.eat())
#> Dog is eating
print(p.eat())
#> Pig is eating

34.3.1 多形的使用時機

  • 課堂上有舉個使用時機,但我現在還看不太懂好處在哪。