正如在了解 Python 中的面向對象編程中所看到的那樣,面向對象編程 (OOP) 是一種使用「對象」來設計應用程序和計算機程序的範式。它利用幾個關鍵概念(包括封裝、繼承和多態性)來提高代碼的模塊化和靈活性。在本文中,我們將重點介紹封裝,這是 OOP 的一個基本方面,有助於實現數據隱藏和抽象。
什麼是封裝?
封裝是將對數據進行操作的數據 (屬性) 和方法 (函數) 捆綁到一個單元(稱為對象)中的機制。它限制對對象的某些組件的直接訪問,這可以防止意外修改數據。要理解封裝,讓我們分解一下它的主要功能:
- 數據隱藏:對象的內部狀態對外界隱藏。這也稱為信息隱藏。
- 訪問限制:外部代碼無法直接訪問對象的內部狀態。相反,它必須使用對象提供的特定方法(如 getter 和 setter)來讀取或修改其狀態。
- 簡化:通過明確定義的介面與對象交互,降低了系統的複雜性,使其更易於理解和維護。
Python 中的封裝
Python 的封裝方法有些獨特。與 C++ 或 Java 等語言不同,Python 沒有 public、private 或 protected 等關鍵字來顯式實施訪問限制。
Python 對類數據和方法採用更開放的方法,基本上將所有 data 和 methods 視為公共的。對於那些習慣了其他語言的嚴格訪問控制的人來說,這種設計選擇似乎不合常規,但它體現了一個核心的 Python 理念:「我們都是同意的成年人。
「我們都是自願的成年人」的原則超出了單純的代碼結構;它反映了整個 Python 社區的精神。它建議開發人員應該相互信任,以負責任地使用類屬性和方法,而不是像 Java 等語言中那樣,通過 private 或 protected 等訪問修飾符來強制實施嚴格的屏障。
但是,Python 支持一種約定來實現類似的效果。
Python 中的命名約定
單前導下劃線:_
第一個也是最廣泛認可的約定是使用單個前導下劃線 (_) 來表示不打算成為類的公共介面一部分的屬性或方法。這些是內部實現,可能會發生變化,不應直接依賴外部代碼。
class MyClass:
def public_method(self):
print("This is a public method")
def _internal_method(self):
print("This is an internal method")
在此示例中,_internal_method 旨在在 MyClass 內部或由子類使用,這表明它不是穩定的公共介面的一部分。雖然 Python 中沒有任何內容阻止對 Python 的訪問_internal_method,但下劃線向其他開發人員發出了一個明確的信號,即應該謹慎使用。
單個下劃線 (_) 用例
在此示例中,_connect_to_Database 是 DatabaseConnector 的內部方法,旨在供類中的其他方法(如 connect)使用,而不是從類外部直接訪問。
class DatabaseConnector:
def connect(self):
self._connect_to_database() # Internal use indicated by single underscore
def _connect_to_database(self):
print("Connecting to the database")
在 Python 中,單個下劃線也用於表示變數是臨時的或無關緊要的。這在循環或解包表達式中很常見,其中一個或多個值是有意未使用的。
for _ in range(5):
print("Repeat action")
_, value = (1, 2) # Only interested in the second value
這裡, _ 用於忽略 loop 變數和 Tuples 中的第一個值,分別關注重複項和感興趣的值。
雙前導下劃線:__
Python 還使用涉及雙前導下劃線 (__) 的命名約定來創建更強的隱私指示。這會觸發一個稱為名稱修飾的功能,其中解釋器以某種方式更改變數的名稱,使其更難(但並非不可能)從類外部訪問。
class MyClass:
def __init__(self):
self.__private_var = "This is a private variable"
def __private_method(self):
print("This is a private method")
使用名稱修飾時,__private_var 和 __private_method 在內部分別重命名為 _MyClass__private_var 和 _MyClass__private_method。此機制不會使屬性或方法真正私有,但確實表明了更強的非公共訪問意圖,從而有效地阻止了來自類外部的直接訪問。其目的是創建不應直接從類外部訪問的「私有」成員的更強指示,從而保護其內部狀態和行為。
雙下劃線 (__) 用例
在此示例中,__balance只能通過 Account 類的方法(如 deposit)進行訪問,以保護帳戶餘額的完整性。
class Account:
def __init__(self, balance):
self.__balance = balance # Name mangling to make it harder to access directly
def deposit(self, amount):
if amount > 0:
self.__balance += amount # Accessing the mangled name internally
account = Account(100)
# Direct access to '__balance' would fail; Python mangles the name to '_Account__balance'
雙下劃線還可用於避免子類中的命名衝突,這些衝突可能會無意中覆蓋基類屬性或方法。
class Base:
def __init__(self):
self.__hidden = 'Base'
class Derived(Base):
def __init__(self):
super().__init__()
self.__hidden = 'Derived' # Does not actually override 'Base.__hidden'
base = Base()
derived = Derived()
# 'Base.__hidden' and 'Derived.__hidden' are two different attributes due to name mangling.
在這裡,Base 類和 Derived 類中的 __hidden 以不同的方式進行分解,確保 Derived 類的 __hidden 屬性不會覆蓋 Base 類的 __hidden 屬性,從而防止意外的副作用。
總之,在 Python 中使用單下劃線 (_) 和雙下劃線 (__) 有不同的目的。單個下劃線是內部使用或忽略值的提示,促進了一種基於約定的方法來指示預期的使用範圍。雙下劃線通過名稱修飾,為表示 「私人」 成員提供了更強的屏障,避免了命名衝突,符合「我們在這裡都是成年人」的原則,同時仍然保護了類的內部運作。
深入研究其他一些示例,以更好地了解 Python 中封裝的工作原理。
class Test:
def __private_symbol(self):
print("This is a private method.")
def normal_symbol(self):
print("This is a normal method.")
# Accessing the public method
t = Test()
t.normal_symbol() # Outputs: This is a normal method.
# Attempting to access the private method directly will result in an AttributeError
# t.__private_symbol() # Uncommenting this line will raise an error
# However, the private method can still be accessed through its mangled name
t._Test__private_symbol() # Outputs: This is a private method.
對於類 Test 中名為 __private_symbol 的方法或變數,Python 會將名稱修改為 _Test__private_symbol。這就是為什麼當你使用 dir(Test) 檢查類的目錄時,你看到的是 _Test__private_symbol 而不是 __private_symbol 的 marmed 名稱。
了解公約
- 私人會員:應被視為 API 的非公共部分。它們旨在成為課程的內部內容,如有更改,恕不另行通知。因此,不建議在外部使用這些成員,以保持代碼兼容性並防止意外誤用。
- 名稱修飾: 此機制並非旨在安全地隱藏信息。相反,這是一種幫助避免子類中的命名衝突的方法,這些衝突可能會無意中覆蓋 superclasses 的私有成員。
考慮一個表示銀行帳戶的簡單類 BankAccount:
class BankAccount:
def __init__(self, account_number, balance=0):
self.__account_number = account_number # private attribute
self.__balance = balance # private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f"Amount {amount} deposited successfully.")
else:
print("Deposit amount must be positive.")
def withdraw(self, amount):
if amount > 0 and amount <= self.__balance:
self.__balance -= amount
print(f"Amount {amount} withdrawn successfully.")
else:
print("Insufficient balance or invalid withdrawal amount.")
def get_balance(self):
return self.__balance
def set_balance(self, balance):
if balance >= 0:
self.__balance = balance
else:
print("Balance cannot be negative.")
在此示例中,BankAccount 類封裝了 __account_number 和 __balance 屬性,使它們成為私有的。這可以防止從類外部直接訪問,從而強制執行封裝。該類提供了 deposit()、withdraw() 和 getter/setter 方法等公共方法,供 __balance 與這些私有屬性進行交互。
數據科學上下文中的封裝:Python 示例
在數據科學中,封裝可能特別有用,例如以安全和結構化的方式管理和操作數據集。讓我們考慮一個在數據科學項目中應用封裝的實際示例。
假設我們正在使用一個數據集來跟蹤網站的用戶參與度指標。我們的目標是將數據集及其操作方法封裝在一個類中,為數據操作提供一個清晰簡單的介面,同時隱藏數據處理的複雜性。
示例:User Engagement Metrics 類
我們將創建一個類 UserEngagementData,用於封裝網站的用戶參與度數據。本課程將提供添加新數據、計算平均參與度指標和檢索特定數據點的方法,同時保持原始數據的私密性。
import pandas as pd
class UserEngagementData:
def __init__(self):
# Initialize a private DataFrame to store user engagement data
self.__data = pd.DataFrame(columns=['user_id', 'page_views', 'time_spent'])
def add_engagement_data(self, user_id, page_views, time_spent):
"""Add a new record of user engagement data."""
new_data = {'user_id': user_id, 'page_views': page_views, 'time_spent': time_spent}
self.__data = self.__data.append(new_data, ignore_index=True)
def average_engagement(self):
"""Calculate average page views and time spent across all users."""
if not self.__data.empty:
return {
'average_page_views': self.__data['page_views'].mean(),
'average_time_spent': self.__data['time_spent'].mean()
}
else:
return {'average_page_views': 0, 'average_time_spent': 0}
def get_user_engagement(self, user_id):
"""Retrieve engagement data for a specific user."""
user_data = self.__data[self.__data['user_id'] == user_id]
if not user_data.empty:
return user_data.iloc[0].to_dict()
else:
return "User data not found."
# Example usage
engagement_data = UserEngagementData()
engagement_data.add_engagement_data(user_id=1, page_views=5, time_spent=120)
engagement_data.add_engagement_data(user_id=2, page_views=3, time_spent=80)
print(engagement_data.average_engagement())
print(engagement_data.get_user_engagement(1))
在此示例中,UserEngagementData 類封裝了用戶參與度數據集(存儲為 pandas DataFrame),並提供與此數據交互的公共方法。
此設計具有以下幾個優點:
- 數據完整性:通過限制對數據集的直接訪問,我們可以防止外部代碼的意外修改,從而確保數據集的完整性。
- 簡單性:該類的用戶不需要了解底層數據結構 (pandas DataFrame) 或用於計算平均值的邏輯。他們通過簡單、直觀的方法與數據集進行交互。
- 靈活性:如果我們決定更改內部實現(例如,切換到不同的數據存儲機制),我們可以在不影響使用此類的代碼的情況下這樣做。
利用屬性裝飾器和 @method_name.setter 在 Python 中進行封裝
在 Python 中,@property 裝飾器是一個內置的裝飾器,它允許我們在類中定義可以像屬性一樣訪問的方法。此功能對於實現封裝特別有用,提供了一種使用 getter 和 setter 來管理對私有變數的訪問的 Pythonic 方法。
了解屬性修飾器
@property 裝飾器將類方法轉換為屬性。這意味著可以像訪問屬性一樣訪問該方法,而無需像調用函數一樣調用它。這特別有用,主要有兩個原因:
- Getter 方法:通過使用 @property 修飾方法,您可以訪問 private 屬性,而無需直接公開它。
- Setter Method:通過使用 @setter 裝飾器,您可以定義一個允許您設置屬性值的 setter 方法。此方法可以包括用於驗證或修改正在設置的數據的邏輯,從而控制如何修改屬性。
示例:對用戶配置文件使用屬性修飾器
讓我們創建一個類 UserProfile,它使用屬性修飾器來管理對用戶年齡的訪問,確保只能分配有效的年齡。
class UserProfile:
def __init__(self, name, age):
self.name = name
self.__age = age # Private attribute
@property
def age(self):
"""Getter method for age."""
return self.__age
@age.setter
def age(self, value):
"""Setter method for age with validation."""
if isinstance(value, int) and 0 < value < 150:
self.__age = value
else:
raise ValueError("Age must be an integer between 1 and 149.")
# Example usage
user = UserProfile("John Doe", 25)
print(user.age) # Accesses the getter method
user.age = 30 # Accesses the setter method
print(user.age)
# Trying to set an invalid age
try:
user.age = -5
except ValueError as e:
print(e) # This will raise the ValueError defined in the setter method
@property 裝飾器和@method_name.setter代碼說明
在此示例中,UserProfile 類具有從外部封裝的 private 屬性 __age。@property 修飾的 age 方法充當 getter,允許我們在不直接訪問私有變數的情況下檢索用戶的年齡。
@age.setter 方法允許設置 __age 的值,還有一個額外的好處是能夠包含驗證邏輯以確保 age 在合理範圍內。如果指定的對象是指定的類型,則 isinstance() 函數返回 True。驗證的另一部分是 Age range (年齡範圍) 的檢查。
這種使用屬性裝飾器和 @method_name.setter 進行封裝,不僅保護了數據的完整性,還為與類屬性的交互提供了一個清晰直觀的界面。它封裝了類的內部工作原理,僅向外部世界公開必要和安全的內容。
Python 中的 @property 裝飾器提供了一種優雅而有效的方法來實現封裝。通過提供對私有變數的受控訪問機制,它有助於維護數據的完整性,同時使類更易於使用和維護。這種封裝方法與面向對象編程的原則非常一致,強調數據隱藏和介面的重要性。
如果您想了解有關 @property裝飾器的更多信息:
了解 Python 中的@property裝飾器
Python 最強大的裝飾器
為什麼使用封裝?
封裝具有以下幾個好處:
- 安全性:敏感數據隱藏起來,不讓外部訪問,從而降低了意外修改的風險。
- 簡單性:對象的客戶端不需要了解其內部複雜性。他們通過一個簡單的界面與之交互。
- 模塊化:將數據和操作封裝在對象中使代碼更加模塊化且更易於維護。
封裝是一個強大的 OOP 概念,可以顯著增強數據科學項目的結構和安全性。通過將數據和操作封裝在類中,我們創建了一個更加模塊化、可維護且易於使用的代碼庫。這種方法在數據管理科學中特別有用,因為在數據科學中,管理複雜的數據集和操作很常見。
封裝是面向對象編程中的一個強大概念,它有助於以捆綁數據和操作的方式構建代碼,限制對內部狀態的訪問,並為交互提供清晰的介面。
在 Python 中,儘管封裝不是由特定於語言的關鍵字強制執行的,但使用屬性修飾器在屬性名稱前加上雙下劃線的約定可以有效地實現此目的。了解和實施封裝可以帶來更安全、模塊化和可維護的代碼庫。