iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0

今天以實例說明何以類別要有私有屬性。實務上私有屬性的機制,對類別究竟有甚麼「貢獻」。


  • 很多人為求方便或並不了解保護層級的概念,在設計類別的時候,所有屬性一律設為公開。
  • 這樣的設計,所有使用者(這時已和類別設計者無關了)都可以在主程式以物件.屬性來存取物件本身的屬性,用起來很方便。
  • 有利就有弊,如此設計可能帶來風險:類別中的屬性由於都是「公開」,就有可能被使用者有意無意間賦予不合理的值。
  • 意思是:這個類別的設計者沒有考慮「防呆」功能。
  • 看code吧。類別的設計(定義)如下,屬性全部「公諸於世」:
    class Tree():   # class
        def __init__(self, breed: str, age: int, height: int):
            self.breed = breed     # public attribute
            self.age = age         # public attribute
            self.height = height   # public attribute
    
  • 類別的使用。目前只是「取值」,不會有甚麼問題:
    trees = []   # list of trees
    trees.append(Tree('cedar', 3_250, 33))              # 造一棵雪松。
    trees.append(Tree('oak', 285, 19))                  # 造一棵橡樹。
    trees.append(Tree('bristlecone pine', 4_854, 10))   # 造一棵刺果松。
    
    # 每一棵tree(不是Tree,也不是trees)都是一個object(or instance)。
    for tree in trees:  
        print(f'{tree.breed=:20}{tree.age=:<10,}{tree.height=}')
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220924/20148485RtBhREsnjj.png
  • 這時再造一棵樟樹,傳入建構子的參數是樹齡600歲,樹高27公尺,本也屬合理範圍。
  • 其後問題來了,這位天馬行空的使用者以物件.屬性直接賦值,將樹齡改為30萬年。30萬年的樟樹不變成「倩女幽魂」中的樹妖姥姥嗎?更不用提樹高竟然是個負數了。
    camphor = Tree('camphor', 600, 27)   # 再造一棵樟樹。
    print(f'\n{camphor.breed=:12}{camphor.age=:<12,}{camphor.height=}')
    camphor.age = 300_000     # 樹妖姥姥
    camphor.height = -50      # 樹高為負要表達甚麼概念?
    print(f'{camphor.breed=:12}{camphor.age=:<12,}{camphor.height=}')
    
    執行後資料真的改掉了:
    https://ithelp.ithome.com.tw/upload/images/20220924/20148485COnjkivvsP.png
  • 當初類別的屬性全設為公開,就會埋下這種地雷。
  • 如果遵循最嚴格的封裝原則,class內的所有屬性都應該是私有:
    class Tree():
        def __init__(self, breed: str, age: int, height: int):
            self.__breed = breed        # private attribute
            self.__age = age            # private attribute
            self.__height = height      # private attribute
    
  • 樹種、樣齡和樹高現在都設為private,保護得很好。
  • 不過以目前的code,物件一旦建立,屬性就固定無法改變(註1)。真要修改怎麼辦?
  • 所以我們還需要一些公開的方法,透過這些方法來存取物件內的私有屬性。
  • 這些存取私有屬性的方法,有時會稱為getters和setters。
    class Tree():
        def __init__(self, breed: str, age: int, height: int):
            self.__breed = breed        # private attribute
            self.__age = age            # private attribute
            self.__height = height      # private attribute
    
        def get_breed(self) -> str:         # public getter for breed
            return self.__breed
    
        def set_breed(self, breed: str):    # public setter for breed
            self.__breed = breed
    
        def get_age(self) -> int:           # public getter for age
            return self.__age
    
        def set_age(self, age: int):        # public setter for age
            self.__age = age
    
        def get_height(self) -> int:        # public getter for height
            return self.__height
    
        def set_height(self, height: int):  # public setter for height
            self.__height = height
    
  • 這樣設計的目的,是透過公開窗口(方法)來管理私有屬性,不能讓樹妖姥姥迫害聶小倩和寧采臣。
  • 以下的code展示如何透過公開的setters來更改私有屬性的值。
    camphor = Tree('camphor', 600, 27)   # 造一棵樟樹。
    print(f'breed: {camphor.get_breed():<12}age: {camphor.get_age():<12,}height: {camphor.get_height():<12}')
    
    camphor.set_breed('cabbage')  # 透過公開的setter來更改私有屬性breed的值
    camphor.set_age(999_999)      # 透過公開的setter來更改私有屬性age的值
    camphor.set_height(-9)        # 透過公開的setter來更改私有屬性height的值
    
    print(f'breed: {camphor.get_breed():<12}age: {camphor.get_age():<12,}height: {camphor.get_height():<12}')
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220924/20148485dHn17SOrH1.png
  • 您可能馬上會說,這不是多餘嗎?設了setters之後,資料還是一樣會錯得離譜呀。要setters何用?
  • 沒錯,如果setters像上面的code那樣,的確和最初的全公開屬性版,直接以物件.屬性方式賦值沒有差異。不只沒有用處,反而累贅。
  • setters當然不是沒用,而是上面的setters太過陽春,完全沒有過濾效果。
  • 改進一下之後,就不一樣了:
    class Tree():
        def __init__(self, breed, age, height):
            self.__breed = breed
            self.__age = age
            self.__height = height
    
        def get_age(self) -> int:      # public getter for age
            return self.__age
    
        def set_age(self, age: int):   # public setter for age
            age_ranges = {'camphor': [0, 800], 'oak': [0, 300]}
            if age < age_ranges[self.__breed][0] or age > age_ranges[self.__breed][1]:
                raise Exception('樹齡數字不合理。')
            else:  # 放行
                self.__age = age
    
  • 這次類別內的set_age()加入了條件,用以過濾不合理的資料。為了簡潔聚焦,上面只示範set_age(),set_breed()和set_height()略過。
  • 主程式如果傳入不合理的樹齡數字:
    camphor = Tree("camphor", 50, 37)
    
    print(f"before set_age(): {camphor.get_age()=}")
    try:
        camphor.set_age(3_000)   # 傳入超出合理範圍的值。
    except Exception as e:
        print(e)
    print(f"after set_age() : {camphor.get_age()=}")
    
    結果是:
    https://ithelp.ithome.com.tw/upload/images/20220924/20148485o57Wgg5NPB.png
  • 傳給set_age()的參數在合理範圍之內時:
    camphor = Tree("camphor", 50, 37)
    
    print(f"before set_age(): {camphor.get_age()=}")
    try:
        camphor.set_age(145)    # 這次傳入在合理範圍以內的值給set_age()
    except Exception as e:
        print(e)
    print(f"after set_age() : {camphor.get_age()=}")
    
    結果成功修改了age屬性的值:
    https://ithelp.ithome.com.tw/upload/images/20220924/20148485Ch5NPDDP6T.png
  • 事實上setters的控制可以更加精密嚴謹,例如除數字範圍外,說不定還可以偵測當時的時間,正常上班以外的時間都不允許程式修改資料,或者任何想得到做得到的檢查控制邏輯,都可以放入setters中。
  • 這樣設計,是否就比將屬性設為public,交由完全沒有攔阻功能的物件.屬性 = ???直接賦值安全多了?
  • 個人認為,這是OO中encapsulation「封裝」的精義。
  • 愚見:個人或團隊開發的系統,不一定要封裝得如些嚴密。但如果是開發給別人使用的套件或模組,就得非常慎重,做好妨呆兼妨災工作。

小總結

  • Python類別的屬性和方法都有public公開和private私有兩個封裝層級。至於protected這層,只是約定俗成,不提也罷。
  • 公開的屬性和方法是開放給外部,以物件.屬性物件.方法表示式存取。其名稱無前綴底線。
  • 私有的屬性和方法僅供類別內部享用,不可以用物件.屬性物件.方法的方式在外部存取。
  • 比較特別的是,Python的私有屬性和方法都有個「後門」,知道門道者依然可以外部存取。不過除非確有需要,筆者並不建議走此後門。
  • 屬性請多用private,方法則多用public。

註1: 假設我們不知道,或專案團隊禁止使用上一篇講的用_Tree__breed存取私有屬性的「後門」。


上一篇
在外部修改私有屬性,真行嗎?
下一篇
Attributes vs Properties
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言