Python OOP - 封裝(Encapsulation)
封裝(Encapsulation)
封裝: 指的是將資料(屬性,例如參數)和操作這些資料的方法(行為,例如Function)封裝在同一個類別中,並透過控制對這些資料的訪問來保護它們。
主要目的是提高系統的安全性和靈活性,並隱藏物件的內部實現細節,從而減少外部對其依賴。
簡單的說,Class就是一種封裝過程。
Python透過 __ 可以宣告成私有屬性或方法,用以宣告該屬性或方法僅能透過內部訪問。
範例
class Person:
def __init__(self, name: str, age: int):
self.__name = name # 私有屬性
self.__age = age # 私有屬性
def get_name(self):
return self.__name
def set_name(self, name: str):
if isinstance(name, str): # 確保輸入值是字串
self.__name = name
else:
raise ValueError("Name must be a string")
def get_age(self):
return self.__age
def set_age(self, age: int):
if 0 < age < 120: # 確保年齡在合理範圍內
self.__age = age
else:
raise ValueError("Invalid age")
person = Person("Leo", 25)
print(person.__name) # attribute error
輸出結果
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 25, in <module>
print(person.__name) # Leo
AttributeError: 'Person' object has no attribute '__name'
範例中, __name 和 __age 屬於私有屬性,僅能透過 get_name() 和 set_name() 等方法來訪問或修改;同理,若將function前加上 __ ,也會報告 AttributeError。
這樣的設計保證了外部無法直接修改物件的屬性,使其成為私有屬性(private attribute)。
Hint
注意: 若宣告單底線 _ 同樣也是私有屬性,但這僅只於讓閱讀程式碼的人知道該屬性或方法屬於私有的,實際程式碼運行,仍然可以呼叫包含單底線的屬性或方法。
裝飾器(Decorator)
@staticmethod
靜態函數,不屬於該物件的實例,無須件立物件即可直接調用,就像普通的Function一樣。
適合用於定義一些物件內的工具,好比說Get current time,你會一直使用到,但這個功能又只是該物件實例需要使用到的工具,不會存取物件內的屬性,就可以定義成靜態方法。
from datetime import datetime
class TimeUtility:
def __init__(self):
pass
@staticmethod # 靜態方法
def get_current_time():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 使用靜態方法來獲取當前時間
current_time = TimeUtility.get_current_time()
print(f"Current Time: {current_time}")
在使用靜態方法上,無論是否有初始化這個物件,都是可以被呼叫使用。
from datetime import datetime
class TimeUtility:
def __init__(self):
pass
@staticmethod # 靜態方法
def get_current_time():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 使用靜態方法來獲取當前時間
current_time = TimeUtility.get_current_time()
print(f"Current Time: {current_time}")
# 使用實例方法來獲取當前時間
current_time = TimeUtility()
print(current_time.get_current_time())
但根據Python的設計慣例,一般建議用類別名稱來呼叫靜態方法,這樣可以表明該方法不依賴於實例的狀態。
這樣的做法雖然不是強制的,但更符合Python開發中常見的實踐方式,提升程式碼的可讀性和邏輯清晰度。
@classmethod
利用@classmethod裝飾器定義的方法,其會有一個cls參數用來指向類內所使用到的類別變數。
所有的實例都可以訪問這個類別變數。
與static method一樣,無須建立物件即可調用。
其用意是可以區分層級。
可以簡單解讀成:類別內部的全域變數。
class Person:
population = 0 # 類別變數,紀錄人數
def __init__(self, name):
self.name = name
Person.population += 1 # 每次創建一個新的人時,增加人口數
@classmethod
def get_population(cls):
return f"There are currently {cls.population} people."
@classmethod
def create_anonymous(cls):
return cls("Anonymous")
# 使用類別來呼叫 classmethod
print(Person.get_population()) # There are currently 0 people.
# 創建新實例,修改類別變數
p1 = Person("John")
print(Person.get_population()) # There are currently 1 people.
# 使用 classmethod 創建匿名對象
anonymous = Person.create_anonymous()
print(anonymous.name) # Anonymous
print(Person.get_population()) # There are currently 2 people.
輸出結果
There are currently 0 people.
There are currently 1 people.
Anonymous
There are currently 2 people.
類別變數 population: 這是一個類別變數,所有實例都可訪與修改問這個變數。每當我們創建一個Person實例時,這個變數就會增加。
get_population透過cls.來存取。
create_anonymous這個方法利用cls來創建一個名稱為”Anonymous”的Person實例。
@property
透過宣告@property,可以將屬性封裝在類的內部。
- 可以將屬性封裝在類內部,使得對外部訪問的控制更加靈活,可防止不當修改屬性。
Getter (@property)
@變數名稱.setter
@變數名稱.deleter
若沒有宣告.setter或.deleter,其變數僅能訪問,無法修改或刪除,達到保護效果。
class Account:
def __init__(self, account_name, initial_balance):
self.account_name = account_name
self._balance = initial_balance # 使用單下劃線表示這是一個受保護的屬性
@property
def balance(self):
"""獲取當前餘額"""
return self._balance
account = Account('John', 100)
print(f"John has $ {account.balance}") # John has $ 100
account.balance = 500 # AttributeError: can't set attribute
輸出結果
John has $ 100
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 14, in <module>
account.balance = 500 # AttributeError: can't set attribute
AttributeError: can't set attribute
當透過@property裝飾器時,該變數會是受保護的變數,必須透過宣告setter或deleter才能修改。
class Account:
def __init__(self, account_name, initial_balance):
self.account_name = account_name
self._balance = initial_balance # 使用單下劃線表示這是一個受保護的屬性
@property
def balance(self):
"""獲取當前餘額"""
return self._balance
@balance.setter
def balance(self, amount):
"""設置餘額,需確保餘額不能為負數"""
if amount < 0:
raise ValueError("餘額不能為負數")
self._balance = amount
account = Account('John', 100)
print(f"John has $ {account.balance}") # John has $ 100
account.balance = 500 # AttributeError: can't set attribute
print(f"John has $ {account.balance}") # John has $ 500
account.balance = -1000 # ValueError: 餘額不能為負數
輸出結果
John has $ 100
John has $ 500
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 22, in <module>
account.balance = -1000 # ValueError: 餘額不能為負數
File "/Users/xiu/Desktop/test.py", line 15, in balance
raise ValueError("餘額不能為負數")
ValueError: 餘額不能為負數
透過.setter可以對該屬性進行修改,同時,也可定義其他邏輯來判斷修改方法是否正確,達到保護效果。
class Account:
def __init__(self, account_name, initial_balance):
self.account_name = account_name
self._balance = initial_balance # 使用單下劃線表示這是一個受保護的屬性
@property
def balance(self):
"""獲取當前餘額"""
return self._balance
@balance.setter
def balance(self, amount):
"""設置餘額,需確保餘額不能為負數"""
if amount < 0:
raise ValueError("餘額不能為負數")
self._balance = amount
@balance.deleter
def balance(self):
"""刪除餘額,這裡可以定義刪除的行為"""
del self._balance
account = Account('John', 100)
print(f"John has $ {account.balance}") # John has $ 100
account.balance = 500 # AttributeError: can't set attribute
print(f"John has $ {account.balance}") # John has $ 500
del account.balance
print(f"John has $ {account.balance}")
輸出結果
John has $ 100
John has $ 500
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 28, in <module>
print(f"John has $ {account.balance}")
File "/Users/xiu/Desktop/test.py", line 9, in balance
return self._balance
AttributeError: 'Account' object has no attribute '_balance'
使用deleter可以刪除整個變數。
通常可以用在刪除陣列中的某個元素、dict的某個key-value,抑或是定義其他刪除行為,好比說定義不可刪除的判斷,以防意外刪除。
主要可以使用在釋放記憶體、不需要再使用到的屬性。
@dataclass
這是一個用於整理資料類別的裝飾器,可以減少撰寫樣板程式碼(boilerplate code)的需求,尤其是用以定義資料類別時。
當專案中有許多資料類別時,這個裝飾器可以幫助我們快速定義資料類別,並且提供了一些方便的方法,例如 __init__、__repr__、__eq__ 等。
dataclass 的定義增加了可讀性,並且減少了錯誤的可能性。
Python 3.7 以上版本才有此功能,並且是內建的模組,相對的有另一個模組 pydantic,也是用來定義資料類別的,dataclass不會檢查資料的合法性,而pydantic則會。
在沒有使用 @dataclass 的情況下,我們需要自己定義 __init__、__repr__ 等方法:
class Student:
def __init__(self, name: str, age: int, grade: int):
self.name = name
self.age = age
self.grade = grade
def __repr__(self):
return f"Student(name={self.name}, age={self.age}, grade={self.grade})"
# 使用範例
student = Student(name="John", age=18, grade=1)
print(student) # Student(name=John, age=18, grade=1)
輸出結果
Student(name=John, age=18, grade=1)
而透過 @dataclass 裝飾器,我們可以簡化這個過程:
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grade: int
student = Student(name="John", age=18, grade=1)
print(student) # Student(name='John', age=18, grade=1)
輸出結果
Student(name='John', age=18, grade=1)
透過定義 @dataclass 可以使代碼更加簡潔,若專案參數較多,不但可以減少撰寫各項資料類別的時間,也可以提高程式碼的可讀性,未來修改上也更加方便。
@dataclass 用法:__post_init__()
__post_init__() 僅限於定義 @dataclass 裝飾器時使用,當物件初始化完成後,會自動執行這個方法。
@dataclass 執行時會自動 __init__() ,並且會在執行完畢後,執行 __post_init__() 方法。
其用途用於初始化後,新增或檢查屬性。
以上面的 Student 類別為例,在沒有定義 @detaclass時:
class Student:
def __init__(self, name: str, age: int, grade: int):
self.name = name
self.age = age
self.grade = grade
self.__check_age()
def __repr__(self):
return f"Student(name={self.name}, age={self.age}, grade={self.grade})"
def __check_age(self):
if self.age < 0:
raise ValueError("Age cannot have negative numbers")
student = Student(name="John", age=18, grade=1)
print(student)
student = Student(name="John", age=-1, grade=1)
輸出結果
在沒有定義 @dataclass 時,當 age < 0 時,需要自行新增判斷式,並且在初始化時執行。
Student(name=John, age=18, grade=1)
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 19, in <module>
student = Student(name="John", age=-1, grade=1) # ValueError: Age must be a positive integer
File "/Users/xiu/Desktop/test.py", line 6, in __init__
self.__check_age()
File "/Users/xiu/Desktop/test.py", line 13, in __check_age
raise ValueError("Age cannot have negative numbers")
ValueError: Age cannot have negative numbers
在定義 @dataclass 時,可以直接透過 __post_init__() 方法來檢查屬性:
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grade: int
def __post_init__(self):
if self.age < 0:
raise ValueError("Age cannot have negative numbers")
student = Student(name="John", age=18, grade=1)
print(student)
student = Student(name="John", age=-1, grade=1)
輸出結果
Student(name='John', age=18, grade=1)
Traceback (most recent call last):
File "/Users/xiu/Desktop/test.py", line 18, in <module>
student = Student(name="John", age=-1, grade=1) # ValueError: Age must be a positive integer
File "<string>", line 6, in __init__
File "/Users/xiu/Desktop/test.py", line 12, in __post_init__
raise ValueError("Age cannot have negative numbers")
ValueError: Age cannot have negative numbers
當然了,除了檢查屬性外,也可以用以定義新的屬性,或是進行其他的操作。
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grade: int
def __post_init__(self):
self.student_id = f"{self.name}_{self.age}"
student = Student(name="John", age=18, grade=1)
print(student.student_id)