本篇繼續複習整理 Python 的類別. 類別是物件的模子 (model), 藍圖 (blueprint), 或樣版 (template), 物件則是依據類別定義於記憶體中所建立的實例 (instance). Python 支援物件導向 (object-oriented programmin) 設計, Python 除了所有原生資料型態 (數值, 字串, 元組, 串列, 字典, 集合) 都是物件, 開發者也可以自訂類別, 也就是自己的資料型態.
物件導向設計這種設計範式 (paradigm) 源自 Simula 語言, 雖然以傳統函數式或程序導向 (procedure-oriented) 設計也能完成軟體專案, 但大型軟體專案傾向採用物件導向方式開發, 因為物件導向具有模組化 (modularization) 與可重用性 (reusable) 等優點, 有助於團隊協力開發, 並能降低後續的維護成本.
本系列之前的筆記參考 :
更多 Python 筆記參考 :
參考書籍 :
- 人工智慧 Python 基礎課 (碁峰, 陳會安)
- 精通 Python (碁峰, 賴屹民譯)
- Python 程式設計入門與運算思維 (新陸, 曹祥雲)
- 一步到位! Python 程式設計 (旗標, 陳惠貞)
- Python 程式設計入門指南 (碁峰, 蔡明志譯)
- Python 也可以這樣學 (博碩, 董付國)
線上教學文件參考 :
十. 類別與物件 :
類別是物件導向設計的基礎, 類別是物件的抽象定義, 物件是類別的具體實例, 說明如下 :
- 類別 (class) :
類別是物件的抽象結構定義, 是包含資料與處理資料之程式碼的結構, 也是用來建立物件的模版, 程式設計者可用類別來自訂資料型態. 類別由屬性 (attribute/property) 與方法 (method) 兩種成員 (member) 組成, 屬性就是物件中保存的資料, 又稱為資料欄位 (data field), 它們描述了物件的靜態特性 (狀態); 而方法則是物件內函式的特稱, 方法描述了物件的動態特性 (行為), 也是物件之間互動的窗口. - 物件 (object) :
類別的實例 (instance), 根據類別的定義在記憶體中所建立的實體, 事實上就是將資料 (變數) 與操作 (函式/方法) 合在一起的組合體. 建立物件具體而言就是透過呼叫類別的建構子 __init__() 來初始化物件, 亦即將變數與方法參考儲存在特定的一塊記憶體中.
物件導向主要有下面三大特性 :
- 封裝 (encapsulation) :
封裝是將資料 (屬性) 與操作 (方法) 組合在一起, 程式只能透過方法存取屬性, 從而達到保護資料的作用. 屬性代表物件的狀態, 而方法則代表物件之行為. 封裝主要的目的是為了隱藏資料與保護程式實作細節, 對使用者來說物件如同一個黑盒子, 只能透過公開的方法呼叫才能存取物件內的資料. 在定義類別時於屬性或方法名稱前加上雙底線 (dunder) 即可將其設為私有 (private, 僅限本身存取), 否則預設為公開 (public). - 繼承 (inheritance) :
定義一個類別時可以指定繼承另一個已定義過的類別, 被繼承的類別稱為父類別 (parent class), 繼承者稱為子類別 (child), 在子類別中呼叫 super() 即可參考到父類別. 只要在建構子方法 __init__() 中用 super().__init__() 呼叫父類別的建構子方法, 子類別就能完全複製父類別的成員 (屬性+方法), 視需要也可再增添子類別專屬的屬性與方法, 也可以重新定義與父類別同名之方法來覆寫 (override) 或擴充繼承自父類別的方法, 達到元件可重用 (reusable) 目標, 避免重新打造輪子. 注意, Python 支援多重繼承, 只要將父類別以逗號隔開傳入子類別當參數即可依序逐一繼承. - 多型 (polymorphism) :
多型為不同之類別但具有同名之方法, 可以先建立一個具有這些同名方法之基礎類別讓這些不同類別繼承然後各自覆寫這些同名方法, 這種方法名稱相同但實作的程式碼功能不同的作法稱為多型, 也稱為同名異式.
1. 定義類別 :
Python 使用關鍵字 class 來定義類別, 語法如下 :
class 類別名稱(父類別1, 父類別2, ...):
'''
類別的說明
'''
類別變數1=值1 # 定義類別變數 (又稱為靜態變數)
類別變數2=值2 # 定義類別變數
....
....
def __init__(self, 參數1, 參數2, ...): # 覆寫根類別 object 之建構子方法
#初始化設定
self.屬性1=參數1 # 定義物件變數 (屬性)
self.屬性2=參數2 # 定義物件變數 (屬性)
.....
def 方法1(self, 參數1, 參數2, ...): # 宣告物件方法 (self 為物件本身, 自動傳入)
#操作1程式碼
@staticmethod # 宣告靜態方法
def 方法2(參數1, 參數2, ...):
#操作2程式碼
@classmethod # 宣告類別方法
def 方法3(cls, 參數1, 參數2, ...): # (cls 為類別本身, 自動傳入)
#操作3程式碼
.....
用法說明如下 :
- 類別的標頭以關鍵字 class 開頭, 後面是類別名稱以及小括弧內的父類別列, 標頭以冒號結束後即跳行縮排開始定義類別內容.
- 類別名稱與變數識別字的命名規則相同, 即只能用英數字與底線, 但首字元不能是數字. 慣例上類別名稱首字母用大寫, 多詞名稱可用駝峰字或底線分隔, 例如 MyClass 或 My_class 等.
- 定義在所有方法外的變數稱為類別變數 (class variable), 又稱為靜態變數 (static variable), 它會被此類別的所有物件共用, 存取類別變數時前面須冠類別名稱, 例如 MyClass.name.
- 類別名稱後面的小括弧內為所繼承的父類別名稱, Python 支援多重繼承, 多個父類別以逗號隔開, 子類別將由左至右依序繼承父類別的所有成員, 遇到同名衝突時以左方的類別之成員優先. 若未指定父類別則預設會繼承 object 類別, 這時可省略小括號.
- 類別的方法有四種 :
(1). 初始化方法 : 即名稱為 __init__() 的方法
(2). 物件方法 (一般方法) : 存取對象為物件變數, 故第一參數必須傳入物件
(3). 靜態方法 : 存取對象為靜態變數, 故不須傳入物件
(4). 類別變數 : - __init__() 是類別的特殊方法, 稱為類別的建構子 (constructor), 每次用類別名稱建立新物件時都會呼叫此方法, 利用傳入參數對物件進行初始化 (設定屬性值). 其第一參數 self 代表要建立之物件, 後面跟著以逗號隔開的參數列, 初始化時要參考 self 來進行屬性值之設定, 例如傳入參數 name 之屬性設定為 self.name=name. 這些屬性又稱為實體變數 (instance variable), 因為它們屬於物件實體. 注意, self 不是 Python 的關鍵字, 使用 self 只是慣例, 可以改用任何合法之識別字. 注意, init 前後為雙底線 (double underscore, 又稱 dunder), 前後為雙底線的識別字為 Python 內部使用.
- 類別內可定義三種方法 :
(1). 物件方法 :
此種方法無任何修飾器, 且第一參數為傳入物件本身 (通常以 self 代表物件), 用來讓方法可存取物件之屬性 (即實體變數), 後面跟著以逗號隔開的參數列. 由於封裝與資料隱藏的需要, 類別基本上會有設定 (setter) 與取得 (getter) 這兩類方法作為外部程式存取物件的窗口, 慣例上前者會用 set 開頭; 後者會用 get 開頭, 為了能存取物件的成員, 所有的物件方法第一參數都應該傳入物件參考 self, 除非該方法不需要存取物件的屬性或呼叫其它方法. 因為 self 的作用範圍 (scope) 遍及類別內的任何地方, 傳入 self 可讓方法之作用範圍也能遍及類別內任何地方. 不過外部程式在呼叫方法時卻不需要理會 slef, 只要傳入 self 後面的其他參數即可 (self 會隱性地自動傳入). 若要在物件方法中存取類別變數須用 self.__class__.類別變數.
(2). 類別方法 :
此方法前面須加上 @classmethod 修飾器, 其第一參數必須傳入類別本身 (通常用 cls 表示), 只能用來存取類別變數, 無法存取物件變數 (因為不能傳入物件). 外部程式碼可用類別名稱或物件名稱呼叫類別方法, 不需建立物件也能用類別名稱直接呼叫.
(3). 靜態方法 :
此方法前面須加上 @staticmethod 修飾器, 與類別方法的差別是不會傳入類別本身, 在類別內只能用類別名稱來存取類別變數, 無法存取物件變數 (因為不能傳入物件). 外部程式碼可用類別名稱或物件名稱呼叫類別方法, 不需建立物件也能用類別名稱直接呼叫.
例如下面是一個最簡單的類別範例 :
class MyClass():
pass
此處類別的內容用關鍵字 pass 佔位, 表示這是一個尚未實作的程式碼區塊, 目前跳過不做任何事情, 其用途只是為了滿足語法結構的完整性而已.
這個 MyClass 類別是一個沒有繼承特定父類別的空類別, 類別名稱後面的小括弧也可以省略 :
class MyClass:
pass
看似沒有父類別, 其實是隱性地自動繼承了 Python 的根類別 object, 也可以明確寫出來, 這在 Python 2 是必要的 :
class MyClass(object):
pass
根類別 object 是 Python 所有物件的父類別 (基礎類別, base/parent class), 可用類別的 __bases__ 屬性檢視, 它會傳回其所有父類別組成之 tuple :
>>> class MyClass():
pass
>>> MyClass.__bases__
(<class 'object'>,)
參考 :
根類別 object 內以特殊名稱定義的屬性與方法, 可用 dir() 檢視其內容 :
>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
這些特殊成員都會被每一個物件繼承.
為程式碼加上註解是良好的開發習慣, 關於類別說明的註解文字是在類別標頭底下, 類別內容開始之前用單引號或雙引號括起來, 這樣就可以用 "類別名稱.__doc__" 屬性取得這段說明, 例如 :
>>> class MyClass():
'''這是 MyClass 類別的說明''' # 單行的說明文字
pass
>>> MyClass.__doc__
'這是 MyClass 類別的說明'
說明文字也會包含跳行字元, 例如 :
>>> class MyClass():
'''
這是 MyClass 類別的說明 # 多行的說明文字
'''
pass
>>> MyClass.__doc__
'\n 這是 MyClass 類別的說明\n '
2. 建立物件 :
定義好類別後即可呼叫與類別同名的函數建立物件, 以上面的空類別為例 :
myobj=MyClass()
此指令會執行下列動作 :
- 檢查類別定義, 在記憶體中建立新物件資料結構, 傳回物件參考 (self)
- 呼叫物件的 __init__() 方法並傳入物件參考與參數執行初始化設定
- 傳回新物件的參考給呼叫者
與物件相關的 Python 常用內建函式如下 :
- dir(obj) : 用來檢視物件 obj 的類別
- dir(obj) : 用來檢視物件的內容
- isinstance(obj, class) : 用來檢查 obj 是否為 class 類別之實例
例如 :
>>> class MyClass(): # 定義一個空類別
pass
>>> myobj=MyClass() # 建立一個 MyClass 類別的物件 (實體)
>>> myobj # 顯示物件參考位址 print(myobj)
<__main__.MyClass object at 0x000001E5C5C9B048>
>>> myobj.__str__() # 與 print(nyobj) 一樣
'<__main__.MyClass object at 0x000001E5C5C9B048>'
>>> myobj.__class__
<class '__main__.MyClass'>
>>> type(myobj)
<class '__main__.MyClass'> # 檢視物件類型為 MyClass
>>> dir(myobj) # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> isinstance(myobj, MyClass) # 檢查 myobj 是否為 MyClass 類別之實例
True
>>> isinstance(myobj, str) # 檢查 myobj 是否為 str 類別之實例
False
此例的 MyClass 是一個空類別, 故所建立的物件也是空的, dir() 所顯示的雙底線開頭結尾物件屬性與方法, 這些都是每個物件內部的都有的特殊成員, 其中比較常用的是 :
- __new__() : 建立物件前會被呼叫, 完畢後再呼叫 __init__() 進行初始化
- __init__() : 物件的建構子, 用來初始化物件
- __str__() : 將物件資訊轉成字串輸出, 等於呼叫 print(obj)
- __class__ : 此物件屬性會參考到此物件之類別本身
- __doc__ : 此物件屬性會參考到類別之說明文字
其中 __init__() 被稱為建構子 (constructor), 當執行建立物件指令時, 解譯器首先會先呼叫 __new__() 方法 (可用來處理物件初始化前之前置作業), 然後呼叫 __init__() 進行初始化, 此兩方法只有在建立物件時被先後自動呼叫一次, 但使用者通常只需要覆寫 __init__() 方法來為物件進行初始化設定, 主要是利用傳入參數來設定物件的屬性值或呼叫其他方法, 例如上面的 MyClass 空類別可以改寫為 :
class MyClass():
def __init__(self, a, b):
self.a=a
self.b=b
def showInfo(self):
print("a=", self.a, "b=", self.b) # 透過 self 物件存取其屬性
這個 MyClass 是具有 a, b 兩個公開 (public) 屬性以及一個公開方法 showInfo() 的新類別. 其中 self 是剛建立的新物件的參考, 類別內的每個方法 (包括 __init__() 也是) 都應該以 self 為第一參數, 因為 self 的作用範圍遍及類別內的任何地方, 有傳入 self 的方法才能存取到物件的屬性. 方法內也可以定義區域變數, 但其作用範圍只限於該方法. 例如 :
>>> myobj=MyClass("Hello", "World") # 傳入 a, b 參數建立 MyClass 物件
>>> myobj
<__main__.MyClass object at 0x000001E5C5C9B160> # 新的物件 (參考位址不同)
>>> type(myobj)
<class '__main__.MyClass'>
>>> dir(myobj) # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'showInfo']
與上面的空物件比較, 多了 a, b, showInfo 三個成員, 由於 Python 類別之成員預設都是公開的 (public, 沒有隱藏), 亦即類別外部程式碼可以存取到內部屬性與方法, 例如 :
>>> myobj.a # 取得屬性 a 之值
'Hello'
>>> myobj.b # 取得屬性 a 之值
'World'
>>> myobj.showInfo() # 呼叫物件方法
a= Hello b= World
>>> myobj.a=123 # 直接更改屬性值
>>> myobj.a
123
>>> myobj.b=456 # 直接更改屬性值
>>> myobj.b
456
>>> myobj.showInfo() # 呼叫物件方法
a= 123 b= 456
注意, 建構子有兩個位置參數 a, b, 在呼叫 MyClass() 建立物件時必須傳入這兩個參數, 否則會出現錯誤, 例如 :
>>> myobj=MyClass() # 建立物件時沒有傳入參數導致錯誤
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'
為了避免此問題可以為 __init__() 方法的參數設定預設值, 例如 :
class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self.a=a
self.b=b
def showInfo(self):
print("a=", self.a, "b=", self.b) # 透過 self 物件存取其屬性
上面範例是在 __init__() 建構子中設定類別的屬性, 也可以透過呼叫自訂的物件方法設定, 例如 :
class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self.setA(a)
self.setB(b)
def showInfo(self):
print("a=", self.a, "b=", self.b) # 透過 self 物件存取其屬性
def setA(self, a):
if not isinstance(a, str):
print("參數 a 必須為字串!")
self.a=""
else:
self.a=a
def setB(self, b):
if not isinstance(b, str):
print("參數 b 必須為字串!")
self.b=""
else:
self.b=b
此處 MyClass 類別自訂了兩個物件方法 setA() 與 setB(), 並且在呼叫它們進行初始化設定前先檢查傳入參數的型別是否為字串, 否則就輸出提示訊息並以空字串填入, 例如 :
>>> class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self.setA(a)
self.setB(b)
def showInfo(self):
print("a=", self.a, "b=", self.b) # 透過 self 物件存取其屬性
def setA(self, a):
if not isinstance(a, str):
print("參數 a 必須為字串!")
self.a=""
else:
self.a=a
def setB(self, b):
if not isinstance(b, str):
print("參數 b 必須為字串!")
self.b=""
else:
self.b=b
>>> myobj=MyClass()
>>> myobj.showInfo()
a= hello b= world
>>> myobj=MyClass('Hello', 'Tony')
>>> myobj.showInfo()
a= Hello b= Tony
>>> myobj=MyClass(123)
參數 a 必須為字串!
>>> myobj=MyClass('123', 456)
參數 b 必須為字串!
下面是一個比較具體用途的圓類別 Circle 範例 :
>>> class Circle(): # 定義圓類別
def __init__(self, radius=1): # 建構子
self.radius=radius # 初始化成員
def get_area(self): # 求面積的方法
return math.pi * self.radius ** 2
def get_perimeter(self): # 求周長的方法
return a * math.pi * self.radius
def set_radius(self, radius): # 設定半徑的方法
self.radius=radius
def get_radius(self): # 取得半徑的方法
return self.radius
此類別含有一個屬性 radius, 以及 get_area(), get_perimeter(), get_radius(), 以及 set_radius() 四個函式共五個成員. 建構子 __init__() 除了第一參數 self (物件參考) 外, 還有一個 radius 參數, 當呼叫 Circle() 建立物件時可傳入引數以便設定圓物件的半徑初始值. 此 radius 參數有設預設值 1, 若呼叫 Circle() 時未傳入半徑即以預設值 1 初始化. 注意, 雖然 __init__() 與 set_radius() 內容看似一樣, 但其用途不同, __init__() 僅在呼叫 Circle() 建立物件時會被呼叫一次, 物件建立後若要更改半徑就必須呼叫 set_radius().
呼叫與類別同名之函數 Circle() 可建立 Circle 類別的一個物件實體, 例如 :
>>> class Circle():
def __init__(self, radius=1):
self.radius=radius
def get_area(self):
return math.pi * self.radius ** 2
def get_perimeter(self):
return a * math.pi * self.radius
def set_radius(self, radius):
self.radius=radius
def get_radius(self):
return self.radius
>>> c=Circle() # 建立 Circle 物件 (預設半徑)
>>> type(c)
<class '__main__.Circle'> # 資料類型為 Circle 類別
>>> dir(c) # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_area', 'get_perimeter', 'get_radius', 'radius', 'set_radius']
此例在建立物件時未傳入半徑參數 radius, 故會建立預設半徑為 1 的 Circle 物件 c, 透過 dir() 指令檢視物件內容可知它有五個自訂的資料成員 (高亮度部分), 我們可以透過呼叫物件方法來存取預設半徑物件的成員, 例如 :
>>> c.get_radius() # 取得半徑
1
>>> c.get_perimeter() # 取得周長
9.42477796076938
>>> c.get_area() # 取得面積
3.141592653589793
建立物件後可以呼叫 set_radius() 方法設定半徑, 這樣呼叫其它方法時結果也會同時改變, 例如 :
>>> c.set_radius(5) # 更改物件的 radius 屬性
>>> c.get_radius() # 取得物件屬性
5
>>> c.get_perimeter() # 取得周長
47.12388980384689
>>> c.get_area() # 取得面積
78.53981633974483
可見面積與周長都改變了.
3. 動態綁定 :
由於 Python 是動態語言, 可在執行時期改變資料的結構, Python 的類別與物件也可以在執行時期動態綁定與修改其成員, 這是與靜態語言如 Java 與 C++ 的物件導向特性不同之處.
例如上面所定義的空類別 MyClass, 可以先建立空物件後再動態綁定新增其屬性與方法 :
>>> class MyClass(): # 定義一個空類別
pass
>>> myobj=MyClass() # 建立空物件
>>> myobj.a="Hello" # 動態設定屬性, 若不存在就建立屬性並賦值
>>> myobj
<__main__.MyClass object at 0x000001E5C5C9B860>
>>> dir(myobj) # 檢視物件內容
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a'] # 已新增了 a 屬性
>>> myobj.a # 印出屬性 a 之值
'Hello'
除了用點運算子直接綁定一個新增屬性外, 也可以呼叫內建函式 setattr() 來設定物件之屬性值, 若該屬性不存在就建立新屬性並與以賦值, 其語法為 :
setattr(物件, 屬性, 值)
例如 :
>>> setattr(myobj, "b", "World") # 設定屬性 b 之值為 "World", 若屬性不存在就建立
>>> dir(myobj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b'] # 新增了 b 屬性
>>> myobj.b
'World'
動態綁定方法需要匯入標準函式庫內建的 types 模組, 利用其 MethodType() 函式將要作為方法的函式綁定到物件中, 其語法如下 :
types.MethodType(函式, 物件)
參考 :
先定義要做為物件方法的函式 showInfo(), 然後匯入 types 模組再進行綁定 :
>>> def showInfo(self): # 定義物件方法
print("a=", self.a, "b=", self.b)
>>> import types # 匯入 types 模組
>>> myobj.showInfo=types.MethodType(showInfo, myobj) # 將函式綁定到物件中
>>> dir(myobj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'showInfo'] # 檢視物件多了 showInfo 成員
>>> myobj.showInfo() # 呼叫物件方法
a= Hello b= World
可見與上面用標準方式於類別中定義好成員再產生物件的結果相同, 雖然動態綁定非常具有彈性, 自由度很高, 但這種用法不符合物件導向的設計理念, 對程式開發與維護並沒有好處, 只適合用在原型測試, 不建議用在軟體專案開發上.
4. 類別變數, 類別方法與靜態方法 :
類別變數是定義在方法外面的變數, 是此類別所建立的物件共享之變數 (每一個物件的該變數值都一樣), 在類別內存取類別變數的方式須視方法之不同而定. Python 類別中的方法有三種 :
- 物件方法
- 類別方法
- 靜態方法
沒有任何修飾器的是物件方法, 又稱為實體方法, 因為必須透過物件實體來呼叫, 其操作對象主要是物件變數, 因此會自動傳入物件本身 (self), 但也可以透過 "self.__class__.類別變數" 來操作類別變數.
類別方法與靜態方法操作的對象都只能是類別變數, 前面有 @classmethod 修飾器的是類別方法, 它會自動傳入類別本身 (cls) 作為第一參數用來存取類別變數; 前面有 @staticmethod 修飾器的是靜態方法, 它與類別方法的差別是它不會自動傳入類別本身, 因此必須使用類別名稱來存取類別變數. 類別方法與靜態方法都屬於類別本身, 因此不須建立物件, 可以直接用類別名稱呼叫.
在物件方法中可以使用兩種方式來存取類別變數 :
- 類別名稱.類別物件名稱
- self.__class__.類別物件名稱
外部程式碼則可以使用類別名稱或物件名稱來存取類別變數 :
- 類別名稱.類別變數名稱
- 物件名稱.類別變數名稱
參考 :
例如 :
>>> class MyClass():
count=0 # 累計物件個數用的類別變數
def __init__(self, name):
self.name=name
MyClass.count += 1 # 建立新物件時就累計加 1
def showInfo1(self):
print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")
def showInfo2(self):
print("物件名稱", self.name, "目前共有", self.__class__.count, "個物件")
此 MyClass 類別有三個物件方法, 建構子方法 __init__() 與 showInfo1() 都使用類別名稱來存取類別變數; showInfo2() 則使用 self.__class__ 屬性先取得類別本身再存取 count, 效果一樣.
>>> myobj1=MyClass("tony") # 建立第一個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 1 個物件
>>> myobj1.showInfo2()
物件名稱 tony 目前共有 1 個物件 # 結果與 showInfo1() 相同
>>> myobj2=MyClass("foo") # 建立第二個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 2 個物件
>>> myobj2.showInfo2()
物件名稱 foo 目前共有 2 個物件
>>> myobj3=MyClass("bar") # 建立第三個物件
>>> myobj1.showInfo1()
物件名稱 tony 目前共有 3 個物件
>>> myobj2.showInfo2()
物件名稱 foo 目前共有 3 個物件
>>> myobj3.showInfo2()
物件名稱 bar 目前共有 3 個物件
>>> MyClass.count # 外部程式用類別名稱讀取類別變數
3
>>> myobj1.count # 外部程式用物件名稱讀取類別變數
3
>>> myobj2.count # 外部程式用物件名稱讀取類別變數
3
>>> myobj3.count # 外部程式用物件名稱讀取類別變數
3
可見外部程式碼不論用類別名稱或物件變數名稱讀取類別變數 count, 其值都是相同的, 因為都是指向同一變數. 類別變數屬於該類別的全部物件共享; 而物件變數 (例如 name) 則屬於個別物件.
類別方法只能用來存取類別變數, 不能存取物件變數 (因為不能傳入 self), 其第一個參數必須是類別本身 (呼叫時自動傳入), 慣例上通常使用 cls (也可以用任何合法名稱), 例如 :
>>> class MyClass():
count=0 # 類別變數
def __init__(self, name):
self.name=name
MyClass.count += 1
def showInfo(self):
print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")
@classmethod # getCount() 的修飾器, 宣告其為類別方法 (無參數)
def getCount(cls):
return cls.count
@classmethod # setCount() 的修飾器, 宣告其為類別方法 (有參數)
def setCount(cls, count):
cls.count=count
>>> MyClass.setCount(0) # 類別方法不須建立物件可直接用類別名稱呼叫
>>> MyClass.getCount() # 類別方法不須建立物件可直接用類別名稱呼叫
0
>>> myobj1=MyClass('tony')
>>> myobj1.showInfo()
物件名稱 tony 目前共有 1 個物件
>>> MyClass.getCount() # 用類別名稱存取
1
>>> myobj1.getCount() # 用物件名稱存取
1
>>> myobj2=MyClass('foo') # 建立第二個物件
>>> myobj2.getCount()
2
>>> myobj2.showInfo()
物件名稱 foo 目前共有 2 個物件
>>> MyClass.getCount() # 以類別名稱呼叫類別方法
2
>>> MyClass.setCount(10) # 呼叫類別方法 (有參數)
>>> myobj2.getCount()
10
>>> myobj1.getCount() # 以物件名稱呼叫類別方法
10
>>> MyClass.getCount() # 以類別名稱呼叫類別方法
10
靜態方法與類別方法同樣都只能用來存取類別變數, 不能存取物件變數, 兩者的差別是靜態方法不會像類別方法那樣自動傳入類別本身 (無 cls 可用), 因此在類別方法內部必須使用類別名稱來存取類別變數, 而外部程式碼可以使用類別名稱或物件名稱來呼叫靜態方法, 即使沒有建立物件, 也可以直接用類別名稱呼叫, 例如 :
>>> class MyClass():
count=0 # 類別變數
def __init__(self, name):
self.name=name
MyClass.count += 1
def showInfo(self):
print("物件名稱", self.name, "目前共有", MyClass.count, "個物件")
@staticmethod # getCount() 的修飾器, 宣告其為靜態方法 (無參數)
def getCount():
return MyClass.count
@staticmethod # setCount() 的修飾器, 宣告其為靜態方法 (有參數)
def setCount(count):
MyClass.count=count
>>> MyClass.setCount(0) # 靜態方法不須建立物件可直接用類別名稱呼叫
>>> MyClass.getCount() # 靜態方法不須建立物件可直接用類別名稱呼叫
0
>>> myobj1=MyClass('tony') # 建立第一個物件
>>> myobj1.showInfo() # 呼叫物件方法
物件名稱 tony 目前共有 1 個物件
>>> myobj2=MyClass('foo') # 建立第二個物件
>>> myobj2.showInfo()
物件名稱 foo 目前共有 2 個物件
>>> myobj1.showInfo()
物件名稱 tony 目前共有 2 個物件
>>> MyClass.getCount() # 以類別名稱呼叫靜態方法
2
>>> myobj1.getCount() # 以物件名稱呼叫靜態方法
2
>>> myobj2.getCount() # 以物件名稱呼叫靜態方法
2
>>> MyClass.setCount(10) # 以類別名稱呼叫靜態方法 (setter)
>>> myobj1.getCount() # 以物件名稱呼叫靜態方法
10
>>> myobj2.getCount() # 以物件名稱呼叫靜態方法
10
>>> MyClass.getCount() # 以類別名稱呼叫靜態方法
10
>>> myobj1.showInfo()
物件名稱 tony 目前共有 10 個物件
>>> myobj2.showInfo()
物件名稱 foo 目前共有 10 個物件
>>>
5. 存取權限控制 :
在物件導向設計中, 資料與程式碼被封裝在類別裡面, 目的是可限制外部程式碼對類別成員的存取權限, 以提升軟體之強固性 (robustness) 與隱蔽性 (privacy), 存取權限分成三類 :
- 公開 (public) :
不限制存取, 外部程式碼也可自由存取, 也會被子類別繼承 - 保護 (protected) :
只限類別內可存取, 也會被子類別繼承 - 私有 (private) :
只限類別內可存取, 不會被子類別繼承
因為 Python 管控存取權限是透過對成員的名稱做特定的規範, 所以 Python 類別的成員在預設情況下都是公開的, 非公開之成員是對名稱做如下之特別處理 :
- 保護 (protected) :
成員以一個底線開頭 (protected), 例如 _name 或 _showInfo() - 私有 (private) :
以兩個底線開頭 (private) : 例如 __name 或 __showInfo()
不過 Python 的保護其實只是掩耳盜鈴, 防君子不防小人, 其所謂的保護只是著眼於一般人較少會用底線開頭來為成員命名, 從而被直接存取的機會較少而已, 例如 :
>>> class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self._a=a # 被保護的屬性
self.__b=b # 私有的屬性
def showInfo(self):
print("a=", self._a, "b=", self.__b) # 透過 self 物件存取其屬性
>>> myobj=MyClass()
>>> myobj._a # 外部程式碼仍可存取被保護的屬性
'hello'
>>> myobj._a=123 # 被保護的屬性可以被更改
>>> myobj._a
123
>>> myobj.showInfo() # 屬性 _a 真的被改變了
a= 123 b= world
可見只要知道屬性名稱是單底線開頭就可以自由地存取它, 資料並沒有真正被隱藏. 反觀雙底線開頭的屬性則會被隱藏, 外部程式碼無法直接存取它, 只能透過公開的方法取得其值 :
>>> myobj.__b # 外部程式碼無法存取私有的屬性
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__b'
>>> myobj.showInfo() # 私有屬性只能透過公開方法取得
a= 123 b= world
如果直接改變私有屬性之值, 雖然不會出現錯誤, 但其實並沒有真的被改變 :
>>> myobj.__b=456 # 企圖更改私有屬性之值
>>> myobj.__b # 檢視似乎值被改變了
456
>>> myobj.showInfo() # 呼叫公開方法 showInfo() 顯示並沒有被改變
a= 123 b= world
單底線開頭的屬性不僅外部程式碼可讀寫 _a 屬性, 也可以用 del 指令刪除它, 例如 :
>>> del myobj._a # 單底線開頭屬性可用 del 刪除
>>> myobj.showInfo()
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "<pyshell>", line 6, in showInfo
AttributeError: 'MyClass' object has no attribute '_a'
但雙底線開頭的私有屬性無法用 del 刪除, 會出現 AttributeError 錯誤訊息, 例如 :
>>> del myobj.__b # 無法刪除私有屬性
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: __b
只有在類別內部才能更改私有屬性之值, 例如重新定義 MyClass 類別, 新增一個設定私有屬性值的 setter 公開方法 set__b() :
>>> class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self._a=a # 被保護的屬性
self.__b=b # 私有的屬性
def showInfo(self):
print("a=", self._a, "b=", self.__b) # 透過 self 物件存取其屬性
def set__b(self, b): # 私有屬性 __b 的公開設定方法
self.__b=b
>>> myobj=MyClass() # 建立物件
>>> myobj.showInfo() # 顯示屬性值
a= hello b= world
>>> myobj.set__b(456) # 呼叫公開方法設定私有屬性之值
>>> myobj.showInfo() # 顯示屬性值
a= hello b= 456
可見私有屬性確實被封裝隱藏在類別內部, 只能透過公開的設定方法才能存取.
不過 Python 的類別對資料的隱藏與保護並沒有像 Java/C++ 那樣嚴密, 因為私有屬性其實是以特定名稱儲存 (在私有屬性名稱前面冠上單底線與類別名稱 "_類別"), 可用 dir() 檢視物件內容看到此屬性, 例如 :
>>> class MyClass():
def __init__(self, a="hello", b="world"): # 設定參數之預設值
self._a=a # 被保護的屬性
self.__b=b # 私有的屬性
def showInfo(self):
print("a=", self._a, "b=", self.__b) # 透過 self 物件存取其屬性
def set__b(self, b): # 私有屬性 __b 的公開設定方法
self.__b=b
>>> myobj=MyClass() # 建立物件
>>> dir(myobj)
['_MyClass__b', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_a', 'set__b', 'showInfo'
用 dir() 可發現, 與 _a 屬性不同的是, 私有屬性 __b 其實是以 _MyClass__b 之名存放, 而非類別定義中的 __b, 所以外部程式碼直接存取 __b 會失敗, 但若改用 _MyClass__b 即可順利存取, 也可以用 del 將私有屬性刪除, 例如 :
>>> myobj.showInfo() # 顯示屬性值
a= hello b= world
>>> myobj._MyClass__b=789 # 用特殊方法修改私有屬性值
>>> myobj._MyClass__b
789
>>> myobj.showInfo() # 確認私有屬性值已被修改
a= hello b= 789
>>> del myobj._MyClass__b # 用 del 可順利刪除冠上 "_類別" 之私有變數
>>> myobj.showInfo()
Traceback (most recent call last): # 呼叫 showInfo() 確認 __b 已經被刪除
File "<pyshell>", line 1, in <module>
File "<pyshell>", line 6, in showInfo
AttributeError: 'MyClass' object has no attribute '_MyClass__b'
同樣地, 私有方法基本上只限類別內部呼叫, 但外部程式碼還是可以用 "_類別__私有方法()" 的方式呼叫, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def foo(self, b): # 公開方法
self.__bar(b) # 類別內才可以呼叫私有方法
def __bar(self, b): # 私有方法
print(self.__a + " " + b)
>>> myobj=MyClass() # 建立物件
>>> dir(myobj)
['_MyClass__a', '_MyClass__bar', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']
用 dir() 檢視物件可知, 私有方法 __bar 其實在物件內部是以 _MyClass__bar 之名作為參考, 而非類別定義中的 __bar, 所以直接呼叫 __bar() 會失敗, 但呼叫 _MyClass__bar() 卻可以 :
>>> myobj.foo('Tony') # 透過呼叫公開方法才能呼叫私有方法
Hello Tony
>>> myobj.__bar('Tony') # 外部程式碼不可直接呼叫私有方法
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__bar' #外部程式碼看不到私有方法
>>> myobj._MyClass__bar('Tony') # 但可以利用特殊管道呼叫私有方法
Hello Tony
由上面測試可知, 外部程式碼無法直接呼叫私有方法, 必須透過類別內的公開方法間接呼叫, 可見 Python 的私有成員命名機制雖然可基本上達到隱藏成員之目的, 但並沒有完全被封裝起來.
雖然私有變數無法被外部存取, 必須透過公開的物件方法, 但可否像存取屬性那樣而非呼叫函式? 可以的, 只要利用 @property 修飾器將此物件方法轉成屬性模式即可.
具體作法是定義一個傳回私有變數值的公開物件方法, 其名稱為私有變數去掉前面的雙底線, 然後在此物件方法前面添加 @property 修飾器將此方法變身為屬性 :
class MyClass():
def __init__(self, a="Hello"):
self.__a=a
@property
def a(self):
return self.__a
此類別定義了一個與私有變數 __a 的詞幹 a 同名的物件方法 a(), 其內容只是單純地傳回私有屬性之值 (唯讀), 然後在 a() 方法前面加上 @property 修飾器, 這樣外部程式碼就可以用 .a 屬性讀取私有屬性 __a 之值, 而非呼叫 a() 方法了, 但只能唯讀, 不可更改, 也不可刪除, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
@property # 修飾器, 將方法 a() 修飾為物件屬性
def a(self):
return self.__a # 傳回私有屬性之 值
>>> myobj=MyClass()
>>> dir(myobj) # 檢視物件內容
['_MyClass__a', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
可見除了真實名稱為 _MyClass__a 的私有變數 __a 外, 還多了一個 a 方法, 但它已被綁定
到屬性, 故要以屬性的方式存取, 而不是呼叫方法, 例如 :
>>> myobj.a # 讀取私有屬性值
'Hello'
>>> myobj.a() # 被修飾器綁定為屬性後無法被呼叫 (not callable)
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
TypeError: 'str' object is not callable
>>> del myobj.a # 無法刪除私有屬性
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute
>>> myobj.a="World" # 無法設定私有屬性之值
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: can't set attribute
可見此 a 屬性只能讀取, 不能設定, 也不能刪除. @property 修飾器其實是內建函式 property() 的 語法糖 (只能用在 getter ), 此唯讀功能也可以用 property() 函式這本尊來實現, 其參數如下 :
attr=property(fget [, fset [, fdel [, fdoc]]])
常用的是前三個參數, fget 為讀取方法 (getter); fset 為設定方法 (setter), fdel 為刪除方法, 若只傳入 fget 表示傳回之屬性為唯讀; 若傳入 fget 與 fset 表示傳回之屬性可讀寫; 若傳入三個參數表示傳回之屬性可讀寫可刪除, 參考 :
property() 函式會傳回一個代表屬性的參考, 須指配一個代表私有變數的公開屬性名稱作為外部程式碼的存取對象, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self): # 定義讀取私有屬性之物件方法
return self.__a
a=property(__getA) # 只傳入第一參數 (唯讀)
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__getA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a # 透過公開屬性讀取私有屬性 __a
'Hello'
>>> myobj.a='World' # 唯讀, 不能設定
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: can't set attribute
>>> del myobj.a # 不可刪除屬性
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute
可見功能與使用 @property 修飾器完全一樣. 注意, property() 的傳回值也可以指配給其他名稱變數例如 v, 因為它是代表私有變數 __a 的公開屬性, 這樣外部程式碼就要用 myobj.v 來存取私有屬性 __a 了.
若要可讀寫私有屬性, 則必須再定義一個設定方法, 並將其傳給 property() 的第二參數 fset, 例如 :
class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self):
return self.__a
def __setA(self, a):
self.__a=a
a=property(__getA, __setA)
此類別定義了兩個私有方法 __getA() 與 __setA() 用來讀寫私有變數 __a, 然後將此兩方法傳入內建函式 property(fget, fset), 將此函式的傳回值指派給類別變數 a, 這樣就可用它來代表私有變數的公開屬性名稱, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self): # 私有取得方法
return self.__a
def __setA(self, a): # 私有設定方法
self.__a=a
a=property(__getA, __setA) # 傳回可讀寫之屬性
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a # 讀取屬性 a
'Hello'
>>> myobj.a='World' # 設定屬性 a
>>> myobj.a
'World'
>>> del myobj.a # 未傳入刪除方法故不能刪除屬性 a
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
AttributeError: can't delete attribute
如果要讓私有變數可讀寫可刪除, 則必須在 property() 函數中傳入 fdel 方法 :
class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self):
return self.__a
def __setA(self, a):
self.__a=a
def __delA(self):
del self.__a
a=property(__getA, __setA, __delA)
此類別定義了一個物件方法 __delA() 用來刪除 __a 屬性, 將此方法傳入 property() 作為第三參數即可使外部程式碼透過其傳回的公開屬性 a 刪除私有 __a, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self):
return self.__a
def __setA(self, a):
self.__a=a
def __delA(self): # 定義刪除私有屬性 __a 之方法
del self.__a
a=property(__getA, __setA, __delA) # 將刪除方法作為第三參數
>>> myobj=MyClass()
>>> dir(myobj)
['_MyClass__a', '_MyClass__delA', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
>>> myobj.a
'Hello'
>>> myobj.a='World' # 透過屬性 a 讀寫私有屬性 __a
>>> myobj.a
'World'
>>> del myobj.a # 刪除屬性 a 即刪除私有屬性 __a
>>> myobj.a # 屬性 a 已被刪除無法存取
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "<pyshell>", line 5, in __getA
AttributeError: 'MyClass' object has no attribute '_MyClass__a'
>>> dir(myobj) # 檢視物件內容已無私有屬性 _a 之真實名稱 _MyClass__a
['_MyClass__delA', '_MyClass__getA', '_MyClass__setA', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']
可見經由傳入可刪除私有屬性 __a 的方法給 property() 的第三參數, 確實可透過刪除屬性 a 來刪除私有屬性 __a.
注意, property() 的傳回值雖然指配給一個類別變數, 但因為它是綁定到物件變數, 所以並不會像一般類別變數一樣成為物件的共同變數, 例如 :
>>> class MyClass():
def __init__(self, a="Hello"):
self.__a=a
def __getA(self):
return self.__a
def __setA(self, a):
self.__a=a
def __delA(self):
del self.__a
a=property(__getA, __setA, __delA)
>>> myobj1=MyClass()
>>> myobj1.a
'Hello'
>>> myobj2=MyClass('World')
>>> myobj2.a
'World'
>>> myobj1.a
'Hello'
此例用 MyClass 類別建立的兩個物件 myobj1 與 myobj2 的屬性 a 值並不同, 顯示雖然 a 在類別定義中的位置屬於類別變數, 但因為它被綁定到私有的物件變數 __a (其實就是 _MyClass__a), 因此實際上它是物件變數, 不同物件的 a 屬性值不會一樣.
6. 繼承 (inheretance) :
程式碼可重用是軟體開發的不變目標, 在程序導向設計中, 程式碼可重用體現在函式的設計上; 而在物件導向設計中則擴展至類別的繼承機制, 這是比單純的函式呼叫更精緻的可重用結構, 從繼承也衍生了多型 (polymorphism) 的概念.
Python 的類別繼承關係體現於類別定義標頭的小括號中 :
class 子類別名稱(父類別名稱):
#定義類別成員
如果沒有要繼承之父類別, 則整個括號也可以省略, 或留一個空括號亦可, 這時預設會繼承 Python 名為 object 的根類別, 故也可以寫明繼承 object.
子類別將繼承父類別的全部非私有方法 (即名稱不是以雙底線開頭的方法), 但卻不會自動繼承其非私有屬性 (即名稱不是以雙底線開頭的屬性), 例如 :
>>> class Parent(): # 定義父類別
def __init__(self, a='foo'):
self.a=a
def set_a(self, a):
self.a=a
def get_a(self):
return self.a
>>> class Child(Parent): # 定義繼承 Parent 之子類別
def __init__(self, b='bar'):
self.b=b
def set_b(self, b):
self.b=b
def get_b(self):
return self.b
上面定義了一個具有公開屬性 a 的父類別 Parent, 以及一個繼承 Parent 且具有公開屬性 b 的子類別 Child, 用 dir() 函式檢視類別內容可知子類別 Child 繼承了父類別的兩個公開方法 get_a() 與 get_b() :
>>> dir(Parent)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_a', 'set_a']
>>> dir(Child)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_a', 'get_b', 'set_a', 'set_b']
可見因為子類別 Child 從父類別繼承了 get_a() 與 set_a(), 所以它共有四個方法. 但 Child 並沒有從 Parent 繼承其公開屬性 a, 例如 :
>>> parent_obj=Parent() # 建立父類別物件
>>> parent_obj.get_a() # 呼叫父類別的物件方法 get_a()
'foo'
>>> child_obj=Child() # 建立子類別物件
>>> dir(child_obj) # 檢視子類別物件內容 (沒有繼承到父類別的屬性 a)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'b', 'get_a', 'get_b', 'set_a', 'set_b'] # 只有屬性 b
>>> child_obj.get_a() # 呼叫繼承自父類別的物件方法 get_a() 失敗
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
File "<pyshell>", line 7, in get_a
AttributeError: 'Child' object has no attribute 'a' # 子類別無屬性 a
>>> child_obj.get_b() # 呼叫父子類別的物件方法 get_b() 成功
'bar'
>>> child_obj.set_a() # 呼叫繼承自父類別的物件方法 set_a() 失敗
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
TypeError: set_a() missing 1 required positional argument: 'a'
由上面測試可知, 父類別的方法會被自動繼承, 但屬性並不會 (檢視物件 child_obj 內容可知它沒有屬性 a), 必須先呼叫 super() 取得父類別參考, 然後呼叫父類別的建構子即可, 修改上面的子類別, 加上呼叫父類別建構子的 super().__init__() 如下 :
>>> class Child(Parent):
def __init__(self, b='bar'):
super().__init__() # 呼叫父類別的建構子
self.b=b
def set_b(self, b):
self.b=b
def get_b(self):
return self.b
重新建立子類別之物件, 用 dir() 檢視物件內容發現已經繼承了父類別的屬性 a, 呼叫繼承自父類別的 get_a() 方法也不會出現錯誤了 :
>>> child_obj=Child()
>>> dir(child_obj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'get_a', 'get_b', 'set_a', 'set_b']
>>> child_obj.get_a() # 呼叫繼承自父類別的 get_a() 方法
'foo'
可見子類別的建構子務必要用 super().__init__() 呼叫父類別的建構子, 這樣才能繼承父類別的非私有屬性.
在繼承父類別時, 私有成員不會被繼承, 例如定義一個具有公開, 保護, 與私有成員之父類別 :
>>> class Parent():
def __init__(self):
self.public_var="父類別的公開屬性"
self._protected_var="父類別的保護屬性"
self.__private_var="父類別的私有屬性"
def public_method(self):
return "父類別的公開方法"
def _protected_method(self):
return "父類別的保護方法"
def __private_method(self):
return "父類別的私有方法"
然後定義一個繼承 Parent 類別的子類別 Child, 並於建構子中呼叫父類別的建構子 :
>>> class Child(Parent): # 繼承父類別 Parent
def __init__(self):
super().__init__() # 呼叫父類別的建構子
接著建立一個子類別物件, 用 dir() 檢視其內容可知父類別的公開與被保護的成員都會被子類別繼承, 可以直接存取; 私有成員則不會被繼承 :
>>> child_obj=Child()
>>> dir(child_obj)
['_Parent__private_method', '_Parent__private_var', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_protected_method', '_protected_var', 'public_method', 'public_var']
>>> child_obj.public_var
'父類別的公開屬性'
>>> child_obj._protected_var
'父類別的保護屬性'
>>> child_obj.public_method()
'父類別的公開方法'
>>> child_obj._protected_method()
'父類別的保護方法'
觀察上面子類別物件的 dir() 結果可知, 其實子類別暗中還是有繼承父類別的私有成員, 只是前面冠上了底線與類別名稱 _Parent 改名了, 就名稱來說也算是沒被繼承 :
>>> child_obj._Parent__private_var # 偷偷存取父類別的私有屬性
'父類別的私有屬性'
>>> child_obj._Parent__private_method() # 偷偷呼叫父類別的私有方法
'父類別的私有方法'
子類別可以覆寫這些繼承來的公開與被保護成員與定義自己的私有成員, 例如 :
>>> class Child(Parent):
def __init__(self):
super().__init__()
self.public_var="子類別的公開屬性"
self._protected_var="子類別的保護屬性"
self.__private_var="子類別的私有屬性"
def public_method(self):
return "子類別的公開方法"
def _protected_method(self):
return "子類別的保護方法"
def __private_method(self):
return "子類別的私有方法"
>>> child_obj=Child()
>>> dir(child_obj)
['_Child__private_method', '_Child__private_var', '_Parent__private_method', '_Parent__private_var', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_protected_method', '_protected_var', 'public_method', 'public_var']
>>> child_obj.public_var
'子類別的公開屬性'
>>> child_obj._protected_var
'子類別的保護屬性'
>>> child_obj._Child__private_var # 偷偷存取私有屬性
'子類別的私有屬性'
>>> child_obj.public_method()
'子類別的公開方法'
>>> child_obj._protected_method()
'子類別的保護方法'
>>> child_obj._Child__private_method() # 偷偷呼叫私有方法
'子類別的私有方法'
可見同名的成員已經被子類別覆寫了, 不過子類別中的同名私有成員並非被覆寫, 而是定義了自己的私有成員. 注意, 一般來說外部程式碼無法直接存取私有成員, 但在 Python 中可利用在名稱前面冠上 "_類別名稱" 的方式存取私有成員.
Python 支援多重繼承, 一個類別可以同時繼承多個類別, 只要在類別定義時將各父類別作為形式參數以逗號隔開傳入即可, 子類別會依照順序自左向右逐一繼承 :
class 子類別名稱(父類別1, 父類別2, ...):
#定義類別成員
從上面範例已知, 即使沒有明確繼承任何父類別, 仍會隱性地繼承名為 object 的根類別.
例如 :
>>> class Parent_Class_1(): # 定義父類別 1
def parent_method_1(self):
print('呼叫父類別 1 的 parent_method_1() 方法')
>>> class Parent_Class_2(): # 定義父類別 2
def parent_method_2(self):
print('呼叫父類別 2 的 parent_method_2() 方法')
>>> class Child_Class(Parent_Class_1, Parent_Class_2): # 定義子類別繼承父類別 1, 2
def child_method(self):
print('呼叫子類別的 child_method() 方法')
>>> child=Child_Class() # 建立子類別物件
>>> child.parent_method_1() # 呼叫父類別之方法 1
呼叫父類別 1 的 parent_method_1() 方法
>>> child.parent_method_2() # 呼叫父類別之方法 2
呼叫父類別 2 的 parent_method_2() 方法
>>> child.child_method() # 呼叫子類別之方法
呼叫子類別的 child_method() 方法
此例先定義兩個父類別 Parent_Class_1 與 Parent_Class_2, 再繼承此二類別定義其子類別 Child_Class, 然後建立子類別物件, 再逐一呼叫從兩個父類別所繼承來的方法以及子類別自己的方法.
要注意的是, Python 的多重繼承是有順序的, 位置越左越優先, 如果各父類別的成員都沒有同名則順序就不是個問題, 因為所有父類別的成員都會被子類別繼承; 但若有同名現象時, 較左的父類別成員會被繼承, 而較右的父類別同名成員則被捨棄.
修改上面範例, 將兩個父類別的方法改為同名的 parent_method(), 子類別繼承時先繼承父類別 1, 再繼承父類別 2, 重新建立物件後呼叫 parent_method() 結果顯示子類別是繼承了父類別 1 的方法, 捨棄了父類別 2 的方法 :
>>> class Parent_Class_1():
def parent_method(self):
print('呼叫父類別 1 的 parent_method() 方法')
>>> class Parent_Class_2():
def parent_method(self):
print('呼叫父類別 2 的 parent_method() 方法')
>>> class Child_Class(Parent_Class_1, Parent_Class_2):
def child_method(self):
print('呼叫子類別的 child_method() 方法')
>>> child=Child_class()
>>> child.parent_method()
呼叫父類別 1 的 parent_method() 方法
如果修改子類別定義, 調換父類別順序, 改為先繼承父類別 2, 則將繼承其 parent_method() 方法 :
>>> class Child_class(Parent_class_2, Parent_class_1):
def child_method(self):
print('呼叫子類別的 child_method() 方法')
>>> child=Child_class()
>>> child.parent_method()
呼叫父類別 2 的 parent_method() 方法
上面範例為方法的繼承, 屬性的繼承也是如此. 可將上面的範例修改如下 :
>>> class Parent_Class_1(): # 父類別 1
def __init__(self):
self._name='Parent Class 1' # 同名屬性
def show_name(self):
print(self._name)
>>> class Parent_Class_2(): # 父類別 2
def __init__(self):
self._name='Parent Class 2' # 同名屬性
def show_name(self):
print(self._name)
>>> class Child_Class(Parent_Class_1, Parent_Class_2): # 子類別優先繼承父類別 1 的屬性
def __init__(self):
super().__init__() # 呼叫父類別建構子以繼承其成員
>>> child=Child_Class()
>>> child.show_name()
Parent Class 1 # 顯示子類別繼承的是父類別 1 的屬性
可見因為在繼承時將父類別 1 擺前面, 所以遇到同名屬性時會優先繼承 Parent_Class_1 的 _name 屬性, 如果把父類別 2 放前面, 則子類別就會優先繼承父類別 2 的 _name 屬性了, 例如 :
>>> class Child_Class(Parent_Class_2, Parent_Class_1): # 優先繼承父類別 2
def __init__(self):
super().__init__()
>>> child=Child_Class()
>>> child.show_name()
Parent Class 2 # 顯示子類別繼承的是父類別 2 的屬性
如果這優先順序因為其他原因不能改, 但 _name 屬性又想繼承父類別 2 的, 則可以在定義子類別時覆寫此屬性, 例如子類別可以改成下面這樣 :
>>> class Child_Class(Parent_Class_1, Parent_Class_2): # 優先繼承父類別 1 的成員
def __init__(self):
super().__init__()
self._name='Parent Class 2' # 覆寫繼承自父類別 1 的 _name 屬性
>>> child=Child_Class()
>>> child.show_name()
Parent Class 2 # 覆寫為父類別 2 之 _name 屬性
7. 多型 (polymorphism) :
多型的概念來自於類別的繼承, 子類別物件透過繼承與覆寫而擁有同名方法, 多型的意思是子類別物件可以被用在代表父類別物件的地方 (意即將子類別物件傳遞給一個代表父類別物件之變數), 當呼叫這個代表父類別物件的同名方法時, Python 解譯器會沿著繼承鏈決定要呼叫哪一個子類別物件的方法. 參考 :
具體來說, 多型通常展現於傳遞給函式的參數或迴圈的迭代變數中, 這參數或變數在形式上雖然代表著父類別物件, 但意義上卻是各個子類別物件, 因此呼叫同名方法時是參考到各子類別所覆寫實作的方法, 故多型又稱為同名異式 (方法名稱相同, 但執行的是不同的程式碼).
在繼承關係中, 達成多型之要件是子類別必須覆寫父類別的方法, 例如 :
>>> class Book(): # 定義父類別
def __init__(self, price):
self.price=price
def getPrice(self): # 物件方法
return self.price
>>> class ComicBook(Book): # ComicBook 類別繼承父類別 Book
def __init__(self, price):
super().__init__(price)
def getPrice(self): # 覆寫父類別之同名方法
return self.price * 0.8
>>> class ComputerBook(Book): # ComputerBook 類別繼承父類別 Book
def __init__(self, price):
super().__init__(price)
def getPrice(self): # 覆寫父類別之同名方法
return self.price * 0.9
>>> books=[ComicBook(100), ComputerBook(100)] # 建立兩個物件串列
>>> books[0].getPrice() # 呼叫同名方法
80.0
>>> books[1].getPrice() # 呼叫同名方法
90.0
>>> total=0 # 總價初始值
>>> for book in books: # 用迴圈計算總價
total += book.getPrice() # 多型: 呼叫各物件之同名方法
>>> total
170.0
此例建立了兩個 Book 的子類別物件, 它們覆寫了繼承自父類別 Book 的同名方法 getPrice() 來計算書打折後的價格 (漫畫書打 8 折, 電腦書打 9 折), 在計算總價的迴圈中呼叫了相同的 book.getPrice() 方法, 看似相同的方法, 但實際上卻會呼叫各物件對應之不同程式碼方法, 故多型又名同形異式.
另外一個可以說明多型概念的範例如下 :
>>> class Shape(): # 父類別
def getArea(self):
pass
>>> class Square(Shape): # 子類別
def __init__(self, width):
self.width=width
def getArea(self):
print("正方形面積=",self.width ** 2)
>>> class Rect(Shape): # 子類別
def __init__(self, width, length):
self.width=width
self.length=length
def getArea(self):
print("矩形面積=",self.width * self.length)
>>> def showArea(shape): # 函式 (子類別物件傳給像是父類別物件之參數)
shape.getArea()
>>> shape=Shape() # 父類別物件
>>> square=Square(10) # 子類別物件 (正方形)
>>> rect=Rect(10, 20) # 子類別物件 (矩形)
>>> showArea(square) # 實際呼叫的是 Square 類別的 getArea()
正方形面積= 100
>>> showArea(rect) # 實際呼叫的是 Rect 類別的 getArea()
矩形面積= 200
此例中子類別 Square 與 Rect 繼承了父類別 Shape 並實作了其方法 getArea(), 我們另外定義了一個顯示面積的函式 showArea(), 其內容只是呼叫傳入物件的同名方法 getArea() 而已, 此處子類別物件被當成參數傳遞給看起來是父類別物件的 shape 參數, 但實際上並不是去呼叫父類別未實作的 getArea() 方法, 而是在執行當下根據傳入物件決定呼叫哪一個子類別物件的 getArea() 方法.
OK, 經過近兩周的學習, 終於把 Python 的物件導向測試整理完畢, 我覺得跟以前學的 Java 物件導向有點不一樣啊! Java 的類別比較嚴謹, 至少在權限控制上一板一眼, Python 就沒這麼麻煩. 對於還沒有機會開發大型軟體專案的我來說還感受不到 Java 嚴謹的好處, 倒是覺得 Python 的類別與物件用法簡單易學.
沒有留言 :
張貼留言