iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0

昨天簡介類別屬性(class attributes),今天則淺談類別方法(class methods)。

筆者談類別方法的「方法」(註1),是從類別屬性切入。


  • 以下是昨天類別屬性的code。為節省篇幅,僅貼部分,完整版本見昨天發文:
    class Tree():
        count = 0         # 放在constructor外面的是class attributes。
        total_age = 0 
        average_age = 0 
    
        def __init__(self, breed: str, age: int):   
            self.__breed = breed
            self.__age = age
    
            Tree.count += 1          # class attributes是用Tree.xxx而非self.xxx
            Tree.total_age += self.age
            Tree.average_age = round(Tree.total_age / Tree.count, 2)
    
            ...   # 以下略
    
  • count, total_ageaverage_age這幾個類別屬性都設成公開,所以主程式可以直接以方法.屬性物件.屬性存取,沒用到property。看來還滿「方便」的。
  • 不過,這好像違反了筆者一直強調屬性盡量私有(private)的封裝保護層級理論。怎辦?改為私有吧:
  • 改為私有,類別屬性當然得補上properties。補好後的完整版本如下:
    class Tree():
        __count = 0         # All class attributes are set to private.
        __total_age = 0 
        __average_age = 0   
    
        def __init__(self, breed: str, age: int):   
            self.__breed = breed
            self.__age = age
    
            Tree.__count += 1          # class attributes是用Tree.xxx而非self.xxx
            Tree.__total_age += self.age
            Tree.__average_age = round(Tree.__total_age / Tree.__count, 2)
    
        @property
        def count(cls) -> int:
            '''The __count property(getter).'''
            return cls.__count
    
        @property
        def total_age(cls) -> int:
            '''The __total_age property(getter).'''
            return cls.__total_age
    
        @property
        def average_age(cls) -> float:
            '''The __average_age property(getter).'''
            return round(cls.__total_age / cls.__count, 2)
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:   
            '''The age property(getter).'''
            return self.__age    ```
    
  • 測試程式如下。請特別注意:這段測試程式,三個類別屬性都是用「物件.屬性」方式存取:
    def show_count_and_average(count: int, total: int, average: float):
        print(f'{count=:<10,}{total=:<10,}{average=:,.2f}')
    
    trees = [Tree('Cedar', 1_520), Tree('oak', 357), Tree('phoebe', 1_806)]
    
    for tree in trees:
        # 注意:以下三個類別屬性都是用「物件.屬性」方式存取。
        # 討論:如果改用「類別.屬性」存取呢?
        show_count_and_average(count=tree.count, total=tree.total_age, average=tree.average_age)   
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20221001/20148485YR9sscsPMN.png
  • 看來這是個happy ending,從此公主和王子過著幸福快樂的生活。
  • 實際生活當然不會那麼順利。筆者要來找自己的碴了:各位還記得昨天提及的兩點嗎?
  • 所謂「共享」就是透過「物件.屬性」的表示式存取。
  • 也可以透過類別本身,即「類別.屬性」表示式直接存取。
  • 昨天的測試程式,筆者還故意分別用不同表示式(類別.屬性物件.屬性)來存取類別屬性
    https://ithelp.ithome.com.tw/upload/images/20221001/20148485xzGNZvwhn6.png
  • 「同理可證」,剛才的測試程式,如改用「類別.屬性」,應該照樣順利存取吧?試一下手氣:
    def show_count_and_average(count: int, total: int, average: float):
        print(f'{count=:<8,}{total=:<10,}{average=:,.2f}')
    
    trees = [Tree('Cedar', 1_520), Tree('oak', 357), Tree('phoebe', 1_806)]
    try:
        for tree in trees:
            # 改用「類別.屬性」存取。
            show_count_and_average(count=Tree.count, total=Tree.total_age, average=Tree.average_age)
    except Exception as e:
        print(str(e))        
    
  • 結果卻收到一張紅單子:
    https://ithelp.ithome.com.tw/upload/images/20221001/20148485hphhGVKGVH.png

類別方法正式登場

  • 賣的關子真是又長又X。繞了一大圈,終於有請今天的女1號:類別方法(class methods)。
  • 在正式介紹女1號前,先修正剛才的程式碼:
    class Tree():
        __count = 0         # 放在constructor外面的是class attributes。
        __total_age = 0 
        __average_age = 0   
    
        def __init__(self, breed: str, age: int):   
            self.__breed = breed
            self.__age = age
    
            Tree.__count += 1          # class attributes是用Tree.xxx而非self.xxx
            Tree.__total_age += self.age
            Tree.__average_age = round(Tree.__total_age / Tree.__count, 2)
    
        @classmethod     # 注意:要加上這個decorator。
        @property
        def count(cls) -> int:
            '''The __count property(getter).'''
            return cls.__count
    
        @classmethod     # @classmethod要在@property的前面。
        @property
        def total_age(cls) -> int:
            '''The __total_age property(getter).'''
            return cls.__total_age
    
        @classmethod
        @property
        def average_age(cls) -> float:
            '''The __average_age property(getter).'''
            return round(cls.__total_age / cls.__count, 2)
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @property
        def age(self) -> int:   
            '''The age property(getter).'''
            return self.__age
    
  • 同樣的測試程式,再貼一次(不貼怕讀者跳來跳去看不方便):
    def show_count_and_average(count: int, total: int, average: float):
        print(f'{count=:<8,}{total=:<10,}{average=:,.2f}')
    
    trees = [Tree('Cedar', 1_520), Tree('oak', 357), Tree('phoebe', 1_806)]
    try:
        for tree in trees:
            # 改用「類別.屬性」存取。
            show_count_and_average(count=Tree.count, total=Tree.total_age, average=Tree.average_age)
    except Exception as e:
        print(str(e))        
    
  • 這次收不到「牛肉乾」了:
    https://ithelp.ithome.com.tw/upload/images/20221001/20148485MLIRxYgvCk.png
  • 關鍵是那個@classmethod裝飾器。

類別方法的目的和特性

  • 既然有類別屬性,各位應該可以連想得到,必然也有類別方法(class methods)。之前介紹的一般方法也可稱為實例方法(instance methods)。
  • 類別方法(有些書稱「共享函數」)的目的,是存取類別內的共享資料(就是類別屬性),或處理整個類別的「公共事務」,而不是處理某特定實例(物件)的「個人隱私」。
  • 類別方法有下列特性:
    • 和類別屬性相同,也是屬於整個類別,而非類別內的任一實例(物件)。
    • 只能存取類別屬性,不能存取物件/實例屬性。而實例方法則既可存取實例屬性,也可存取類別屬性。
    • 不可以在類別方法內呼叫實例方法。反之,實例方法卻可以呼叫類別方法。
    • 既可以用物件.方法呼叫,也可以經由類別.方法直接呼叫。不過筆者個人建議最好用類別.方法方式呼叫,較能彰顯這是類別方法而非一般的實例方法
    • 類別方法的前面要加一個@classmethod裝飾器
    • 如果既是類別方法也是property,那麼@classmethod要放在@property之前。
  • PEP 8對實例方法和類別方法的第一個參數名稱的規範是:

    Always use self for the first argument to instance methods.
    Always use cls for the first argument to class methods.

  • 所以下圖類別方法(也是property)的第一個參數,筆者都用cls而不是大家熟悉的self
    https://ithelp.ithome.com.tw/upload/images/20221001/20148485giz8F2kRXU.png
  • 類別方法淺談就此打住。明天換一個開胃小菜。

註1: 筆者之前好像有說過,「方法」一詞過於「通用」,個人不大喜歡當作物件導向世界的專門術語,有時寧願用回「函數」。此句就是個例子。


上一篇
Class Attributes
下一篇
To Self, or Not To Self: That Is the Question
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言