iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0

Python的封裝介紹差不多「開到荼靡」了。今天起分享的也許是最後一個大主題(註1)。


  • 在正式介紹前,各位請沿用以上Tree類別,設想一下這個需求:

    • 使用者已利用Tree類別建立了一些樹的實例(instances)。如果資料不存入任何外部檔案或資料庫,如何得知在程式某一階段已經建立了「多少棵樹」?
    • 如何求取所有已建立的樹的平均樹齡?
  • 利用之前講過的知識和技術,上述兩個「簡單」問題,處理起來還真有點不簡單。問題在於:每棵樹都是獨立個體,樹與樹之間好像並無甚麼關連可供計算。

  • 難道要像以下的code,竟然出動到一些外部變數去記錄和計算樹的數目和平均樹齡?唔,這顯然不是個好辦法(註2)。

    class Tree():
        def __init__(self, breed: str, age: int):   
            self.__breed = breed
            self.__age = age
    
        @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_age: int):
        print(f'{count=:<10,}{total_age=:<10,}average={total_age / count:,.2f}')
    
    tree_count = 0         # 利用類別外部的變數記錄樹的數目。
    total_tree_age = 0     # 記錄總樹齡。
    
    tree1 = Tree('Cedar', 1_520)
    tree_count += 1
    total_tree_age += tree1.age
    show_count_and_average(tree_count, total_tree_age)
    
    tree2 = Tree('oak', 357)
    tree_count += 1
    total_tree_age += tree2.age
    show_count_and_average(tree_count, total_tree_age)
    
    tree3 = Tree('phoebe', 1806)
    tree_count += 1
    total_tree_age += tree3.age
    show_count_and_average(tree_count, total_tree_age)
    

    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220930/20148485ibLVRZr1Rk.png

  • 也許您會說:不是有人說過一句名言「不管黑貓白貓,捉到老鼠就是好貓」嗎?需求能夠達成就好。

好貓不是每次都捉到老鼠

  • 我不知道以上這隻貓是黑是白,但我敢說它不小心讓老鼠跑掉的機會不小。因為寫程式的貓,不,是寫程式的人只要一個不留神,少算一次,答案就不正確。這是非常「不靠譜」的做法。
  • 鑼鼓聲中,今天的正印花旦「類別屬性」踩著蹺,以優美身段踏出虎度門:
    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)
    
        @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_age: int):
        print(f'{count=:<10,}{total_age=:<10,}average={total_age / count:,.2f}')
    
    tree1 = Tree('Cedar', 1_520)
    # Tree.count和tree1.count指向的是同一塊memory。這裡筆者故意寫作Tree.count / tree1.total_age,其實Tree.count / Tree.total_age,tree1.count / Tree.total_age,或者tree1.count / tree1.total_age都可以。當然最理想的表示式應該是Tree.count / Tree.total_age,明確指出count和total_age這兩個是class level的attributes。
    show_count_and_average(Tree.count, tree1.total_age)  
    
    tree2 = Tree('oak', 357)
    show_count_and_average(tree2.count, Tree.total_age)
    
    tree3 = Tree('phoebe', 1806)
    show_count_and_average(tree1.count, tree1.total_age)
    
    以上這個「類別屬性」版,輸出和「外部變數版」完全相同:
    https://ithelp.ithome.com.tw/upload/images/20220930/20148485ibLVRZr1Rk.png

屬性其實有兩種

  • 「斯斯」有兩種,「屬性」也有兩種:
    • 實例屬性(instance attributes, 就是本篇之前講的那些屬性),每一個實例(物件)都有一份,是實例自己的「私房錢」,別的實例休想分它一杯羹。
    • 類別屬性(class attributes, class level attributes, 或稱static attributes)。這些屬性只有一份,是屬於整個類別,而不屬於類別內的任何一個物件(實例)。
  • 所有透過該類別建立的物件(註3)可共享類別屬性。
  • 所謂「共享」就是透過「物件.屬性」的表示式存取。
  • 也可以透過類別本身,即「類別.屬性」表示式直接存取。
  • 不管是經由物件或類別本身,都得遵從以上說過的封裝保護層級的原則。

Class Attributes的位置

  • 以前我們寫實例屬性時,都將屬性的初值設定放在建構子constructor之內。但是類別屬性卻不一樣,它們必須放在建構子的「外面」,前置後置都行,就是不能位於constructor裡面
    class Tree():
        count = 0      # 放在constructor外面(前置)的是class attributes。
        total_age = 0 
        average_age = 0    
        def __init__(self, breed: str, age: int):   # constructor
            ...
    
    或:
    class Tree():
        def __init__(self, breed: str, age: int):   # constructor
            ...
        count = 0      # 放在constructor外面(後置)的是class attributes。
        total_age = 0 
        average_age = 0    
    
  • 不過,類別屬性一般都寫在建構子的前面
  • 今天到此為止,有關類別屬性的其他細節,且聽下回分解。

註1: 「封裝」這個主題,真要慢慢磨還有些小題目可以講,不過太過支節的東西並不適合納入本系列。先預告:在封裝的最後一講筆者真的要講個很支節的小題目,輕鬆一下。

註2: 為了聚焦於今天的主題,前兩天介紹的dataclass decorator暫不使用,以傳統的方法來設計類別。

註3: 我發覺講「實例」有點不自然,還是回歸「物件」好了。


上一篇
More about Dataclass Decorator
下一篇
Class Methods
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言