Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數

加入好友
加入社群
Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

在 Python 的物件導向設計中,類別變數是屬於類別本身的屬性,所有實例理論上會共享它。
以圓周率 PI 為例,它是一個適合作為類別層屬性的常數。
但是存取它的方式有很多種,常見的有三種:

  1. self.PI
  2. __class__.PI(或硬寫 ClassName.PI
  3. self.__class__.PI

這三種方法的差別主要體現在 屬性查找路徑修改時的影響範圍


範例類別

class Circle:
    PI = 3.14159  # 類別變數所有實例共享

    def __init__(self, radius):
        self.radius = radius
    
    def area_with_self(self):
        return self.radius ** 2 * self.PI

    def area_with_class(self):
        return self.radius ** 2 * self.__class__.PI  # 動態取得類別屬性

情境對比範例

c1 = Circle(2)
c2 = Circle(2)

#  c1 上建立實例自己的 PI 屬性
c1.PI = 3.0

print(c1.area_with_self())   # 12.0用到 c1 自己的 PI
print(c2.area_with_self())   # 12.56636用到類別層的 PI

print(c1.area_with_class())  # 12.56636直接找類別層 PI忽略實例上的 PI
print(c2.area_with_class())  # 12.56636

# 修改類別層 PI
c1.__class__.PI = 3.0  #Circle.PI = 3.0

print(c1.area_with_class())  # 12.0類別 PI 改了全部實例受影響
print(c2.area_with_class())  # 12.0

輸出:

Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

三種方式差異表

Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

self.__class__. 與另外兩種 (self.ClassName.) 在實務中會有明顯區別的例子。

🔹 範例 1:繼承多型差異(self.__class__ vs ClassName

# %%
class Circle:
    PI = 3.14159
    def __init__(self, radius):
        self.radius = radius

    def area_with_classname(self):
        return self.radius ** 2 * Circle.PI   # 固定父類別 PI

    def area_with_self_class(self):
        return self.radius ** 2 * self.__class__.PI  # 動態取當前類別 PI


class SpecialCircle(Circle):
    PI = 3.14  # 略微不同的 PI


c = SpecialCircle(2)
print(c.area_with_classname())   # 12.56636 → 用的是 Circle.PI父類別值
print(c.area_with_self_class())  # 12.56    → 用的是 SpecialCircle.PI子類別值

輸出:

Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

差異重點

  • Circle.PI 寫死了父類別,子類別 PI 再怎麼改,方法也不會用到它
  • self.__class__.PI 會依照當前實例的實際類別抓對應的值,所以繼承場景更靈活
  • 如果將來改類別名稱,也不必全局替換程式碼

🔹 範例 2:實例覆蓋差異(self. vs self.__class__.

class Circle:
    PI = 3.14159
    def __init__(self, radius):
        self.radius = radius

    def area_with_self(self):
        return self.radius ** 2 * self.PI  # 先找實例屬性

    def area_with_self_class(self):
        return self.radius ** 2 * self.__class__.PI  # 永遠找類別層


c1 = Circle(2)
c2 = Circle(2)

c1.PI = 3.0  # 只針對 c1 建立 PI 實例屬性

print(c1.area_with_self())        # 12.0   →  c1 自己的 PI
print(c2.area_with_self())        # 12.56636 → 用類別層的 PI
print(c1.area_with_self_class())  # 12.56636 → 忽略 c1  shadow 屬性取類別層值

輸出:

Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

差異重點

  • self.PI → 容易被實例自己的屬性遮蔽(shadowing)
  • self.__class__.PI → 永遠取當前類別的屬性,不會被實例屬性干擾

✅ 為什麼推薦 self.__class__. 存取類別變數

  1. 繼承安全 → 子類別覆寫同名屬性會自動適配
  2. 名稱變更免煩惱 → 改類別名稱不必全局替換程式碼
  3. 避免 shadowing 問題 → 不怕實例意外創建同名屬性干擾讀值

圖解:屬性查找與修改影響

以下示意圖展示了三種寫法在查找與修改上的行為差異:

self.PI

   ├─> [實例屬性字典] → 找到就用

   └─> [類別屬性字典] → 找不到才用

 ClassName.PI / __class__.PI

   └─> [指定類別屬性字典] → 忽略實例屬性  

 self.__class__.PI

   ├─> self.__class__實例的實際類別
   └─> [該類別屬性字典] → 忽略實例屬性

實務建議

  1. 允許個別覆蓋的情境 → 用 self.
    • 適合「預設共享,但允許部分實例自訂」
  2. 需要全域固定值 → 用 self.__class__.(推薦)
    • 適合繼承結構,子類別依然能拿到自己定義的值
  3. 快速寫固定類別的值 → 用 ClassName.__class__.
    • 僅在沒有繼承或改類名風險時使用

💡 小技巧

  • 在類別方法 (@classmethod) 內,要存取類別變數時使用 cls.PI,這就是類別方法的等價安全寫法
  • 在實例方法內若想安全取類別變數,推薦 self.__class__.PI
加入好友
加入社群
Python 物件導向(OOP):深入比較 self.、__class__. 與 self.__class__. 存取類別變數 - 儲蓄保險王

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡

You may also like...

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *