你是否曾經想過 Python 如何使物件能夠與像 +- 這樣的運算符一起工作?或者它是如何知道在打印物件時如何顯示它們的?答案在於 Python 的魔法方法,也稱為 dunder(雙下劃線)方法。

魔法方法是特殊的方法,讓你可以定義你的物件如何對各種操作和內建函數做出反應。它們使得 Python 的物件導向程式設計變得如此強大和直觀。

在本指南中,你將學習如何使用魔法方法來創建更優雅和強大的代碼。你將看到實際的範例,展示這些方法在現實世界情況下如何運作。

前提條件

  • 對 Python 語法和物件導向程式設計概念的基本理解。

  • 熟悉類別、物件和繼承。

  • 了解內建的 Python 數據類型(列表、字典等)。

  • 建議安裝一個可運行的 Python 3,這樣可以積極參與此處的範例。

內容表

  1. 什麼是魔術方法?

  2. 物件表示法

  3. 運算子重載

  4. 容器方法

  5. 屬性訪問

  6. 內文管理器

  7. 可呼叫對象

  8. 高级魔术方法

  9. 性能考量

  10. 最佳實踐

  11. 總結

  12. 參考文獻

什麼是魔法方法?

在 Python 中,魔法方法是以雙下劃線(__)開頭和結尾的特殊方法。當你對對象使用某些操作或函數時,Python 會自動調用這些方法。

例如,當你對兩個對象使用 + 運算符時,Python 會在左操作數中查找 __add__ 方法。如果找到,則會以右操作數作為參數調用該方法。

以下是一個簡單的示例,展示了這是如何工作的:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2  # 這會調用 p1.__add__(p2)
print(p3.x, p3.y)  # 輸出:4 6

讓我們來分析一下這裡發生了什麼:

  1. 我們創建了一個 Point 類,表示 2D 空間中的一個點

  2. 這個 __init__ 方法初始化 x 和 y 坐標

  3. 這個 __add__ 方法定義了當我們相加兩個點時會發生什麼

  4. 當我們寫 p1 + p2 時,Python 會自動調用 p1.__add__(p2)

  5. 結果是一個新的 Point,坐標為 (4, 6)

這只是開始。Python 有許多魔法方法,讓你可以自定義對象在不同情況下的行為。讓我們探索一些最有用的方法。

對象表示

當你在 Python 中處理對象時,通常需要將它們轉換為字符串。當你打印對象或嘗試在互動控制台中顯示它時,就會發生這種情況。Python 為此提供了兩個魔法方法: __str____repr__

str 與 repr

__str____repr__ 方法有不同的用途:

  • __str__:由 str() 函數和 print() 函數調用。它應該返回一個對最終用戶可讀的字符串。

  • __repr__:由 repr() 函數調用並用於互動控制台。它應該返回一個字符串,理想情況下可以用來重建對象。

這裡有一個顯示區別的例子:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __str__(self):
        return f"{self.celsius}°C"

    def __repr__(self):
        return f"Temperature({self.celsius})"

temp = Temperature(25)
print(str(temp))      # 輸出:25°C
print(repr(temp))     # 輸出:Temperature(25)

在這個例子中:

  • __str__ 返回一個友好的字符串,顯示帶有度數符號的溫度

  • __repr__ 返回一個顯示如何創建對象的字符串,這對於調試很有用

當你在不同上下文中使用這些對象時,區別變得明顯:

  • 當你打印溫度時,你會看到友好的版本: 25°C

  • 當你在 Python 控制台檢查對象時,你會看到詳細版本: Temperature(25)

實用範例:自定義錯誤類別

讓我們創建一個自定義錯誤類別,提供更好的調試信息。這個例子顯示了如何使用 __str____repr__ 使你的錯誤信息更加有幫助:

class ValidationError(Exception):
    def __init__(self, field, message, value=None):
        self.field = field
        self.message = message
        self.value = value
        super().__init__(self.message)

    def __str__(self):
        if self.value is not None:
            return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
        return f"Error in field '{self.field}': {self.message}"

    def __repr__(self):
        if self.value is not None:
            return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
        return f"ValidationError(field='{self.field}', message='{self.message}')"

# 用法
try:
    age = -5
    if age < 0:
        raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
    print(e)  # 輸出:錯誤在 'age' 欄位:年齡必須為正數(得到:-5)

這個自訂錯誤類別提供了幾個好處:

  1. 它包含了錯誤發生的欄位名稱

  2. 它顯示了引起錯誤的實際值

  3. 它提供了既人性化又詳細的錯誤訊息

  4. 它通過包含所有相關資訊來使調試更容易

運算符重載

運算符重載是 Python 的魔法方法中最強大的功能之一。它允許你定義對象在使用像 +-*== 這樣的運算符時的行為。這使得你的代碼更直觀和可讀。

算術運算符

Python 為所有基本算術操作提供了魔法方法。下表顯示了每個運算符對應的方法:

運算符 魔法方法 描述
+ __add__ 加法
- __sub__ 減法
* __mul__ 乘法
/ __truediv__ 除法
// __floordiv__ 地板除法
% __mod__ 模數
** __pow__ 冪次運算

比較運算子

同樣地,您可以使用這些魔法方法來定義對象的比較方式:

運算子 魔法方法 描述
== __eq__ 等於
!= __ne__ 不等於
< __lt__ 小於
> __gt__ 大於
<= __le__ 小於等於
>= __ge__ 大於等於

實際例子:貨幣類別

讓我們創建一個 Money 類別,正確處理貨幣操作。這個例子展示了如何實現多個運算符並處理邊界情況:

from functools import total_ordering
from decimal import Decimal

@total_ordering  # 根據 __eq__ 和 __lt__ 實現所有比較方法
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = Decimal(str(amount))
        self.currency = currency

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount * Decimal(str(other)), self.currency)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount / Decimal(str(other)), self.currency)
        return NotImplemented

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.currency == other.currency and self.amount == other.amount

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
        return self.amount < other.amount

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({repr(float(self.amount))}, {repr(self.currency)})"

讓我們來分解這個 Money 類別的關鍵特性:

  1. 精確度處理: 我們使用 Decimal 而不是 float 以避免貨幣計算中的浮點精度問題。

  2. 貨幣安全性: 該類別防止不同貨幣之間的操作,以避免錯誤。

  3. 類型檢查: 每個方法都使用 isinstance() 檢查另一個操作數是否為正確類型。

  4. 未實現: 當一個操作不合邏輯時,我們返回 NotImplemented 以讓 Python 嘗試反向操作。

  5. @total_ordering: 此裝飾器自動根據 __eq____lt__ 實現所有比較方法。

以下是如何使用 Money 類別的示例:

# 基本算術
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining)  # 輸出:USD 80.00

# 處理不同貨幣
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total)  # 輸出:USD 6000.00

# 以標量進行除法
weekly_pay = salary / 4
print(weekly_pay)  # 輸出:USD 1250.00

# 比較
print(Money(100, "USD") > Money(50, "USD"))  # 輸出:True
print(Money(100, "USD") == Money(100, "USD"))  # 輸出:True

# 錯誤處理
try:
    Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
    print(e)  # 輸出:無法添加不同貨幣:USD 和 EUR

Money 類別展示了幾個重要概念:

  1. 如何處理不同類型的操作數

  2. 如何實現適當的錯誤處理

  3. 如何使用 @total_ordering 裝飾器

  4. 如何在財務計算中保持精確性

  5. 如何提供字符串和表示方法

容器方法

容器方法讓您可以讓對象像內建容器(如列表、字典或集合)一樣運作。這在您需要自定義存儲和檢索數據的行為時特別有用。

序列協議

要讓對象像序列(如列表或元組)一樣運作,您需要實現這些方法:

方法 描述 範例用法
__len__ 返回容器的長度 len(obj)
__getitem__ 允許用 obj[key] 進行索引 obj[0]
__setitem__ 允許用 obj[key] = value 進行賦值 obj[0] = 42
__delitem__ 允許用 del obj[key] 進行刪除 del obj[0]
__iter__ 返回容器的迭代器 for item in obj:
__contains__ 實現in運算子 42 in obj

映射協議

為了實現類似字典的行為,您需要實現以下方法:

方法 描述 示例用法
__getitem__ 通過鍵獲取值 obj["key"]
__setitem__ 通過鍵設置值 obj["key"] = value
__delitem__ 刪除鍵值對 del obj["key"]
__len__ 獲取鍵值對的數量 len(obj)
__iter__ 遍歷鍵 for key in obj:
__contains__ 檢查鍵是否存在 "key" in obj

實用示例:自定義緩存

我們來實現一個基於時間的緩存,自動清理舊的項目。這個示例展示了如何創建一個行為類似字典但具有額外功能的自定義容器:

import time
from collections import OrderedDict

class ExpiringCache:
    def __init__(self, max_age_seconds=60):
        self.max_age = max_age_seconds
        self._cache = OrderedDict()  # {key: (value, timestamp)}

    def __getitem__(self, key):
        if key not in self._cache:
            raise KeyError(key)

        value, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            raise KeyError(f"Key '{key}' has expired")

        return value

    def __setitem__(self, key, value):
        self._cache[key] = (value, time.time())
        self._cache.move_to_end(key)  # 移動到末尾以維持插入順序

    def __delitem__(self, key):
        del self._cache[key]

    def __len__(self):
        self._clean_expired()  # 在報告長度之前清理過期項目
        return len(self._cache)

    def __iter__(self):
        self._clean_expired()  # 在迭代之前清理過期項目
        for key in self._cache:
            yield key

    def __contains__(self, key):
        if key not in self._cache:
            return False

        _, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            return False

        return True

    def _clean_expired(self):
        """Remove all expired entries from the cache."""
        now = time.time()
        expired_keys = [
            key for key, (_, timestamp) in self._cache.items()
            if now - timestamp > self.max_age
        ]
        for key in expired_keys:
            del self._cache[key]

讓我們來解析這個快取是如何運作的:

  1. 儲存: 快取使用 OrderedDict 來儲存鍵值對及其時間戳。

  2. 過期: 每個值以 (value, timestamp) 的元組形式儲存。當訪問一個值時,我們會檢查它是否已過期。

  3. 容器方法: 該類實現了所有必要的方法以表現得像字典:

    • __getitem__: 檢索值並檢查過期

    • __setitem__: 儲存當前時間戳的值

    • __delitem__: 刪除條目

    • __len__: 返回未過期條目的數量

    • __iter__: 遍歷未過期的鍵

    • __contains__: 檢查鍵是否存在

以下是如何使用快取:

# 建立一個具有2秒過期時間的快取
cache = ExpiringCache(max_age_seconds=2)

# 儲存一些值
cache["name"] = "Vivek"
cache["age"] = 30

# 存取值
print("name" in cache)  # 輸出: True
print(cache["name"])    # 輸出: Vivek
print(len(cache))       # 輸出: 2

# 等待過期
print("Waiting for expiration...")
time.sleep(3)

# 檢查已過期的值
print("name" in cache)  # 輸出: False
try:
    print(cache["name"])
except KeyError as e:
    print(f"KeyError: {e}")  # 輸出: KeyError: 'name'

print(len(cache))  # 輸出: 0

這個快取實作提供了幾個好處:

  1. 自動過期舊的項目

  2. 類似字典的介面,方便使用

  3. 通過移除過期的項目提高記憶體效率

  4. 線程安全的操作(假設單線程訪問)

  5. 保持插入順序

屬性存取

屬性訪問方法讓您控制對象如何處理屬性的獲取、設置和刪除。這對於實現屬性、驗證和記錄特別有用。

getattr和getattribute

Python提供了兩種控制屬性訪問的方法:

  1. __getattr__:當屬性查找失敗時調用(即屬性不存在時)

  2. __getattribute__:對於每個屬性訪問都調用,即使屬性存在

關鍵區別在於,__getattribute__在所有屬性訪問時都被調用,而__getattr__只有在通常的方式下找不到屬性時才被調用。

這是一個簡單的示例,展示了兩者之間的區別:

class AttributeDemo:
    def __init__(self):
        self.name = "Vivek"

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        return f"Default value for {name}"

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

demo = AttributeDemo()
print(demo.name)      # 輸出:調用了__getattribute__,屬性名稱為name
                      #        Vivek
print(demo.age)       # 輸出:調用了__getattribute__,屬性名稱為age
                      #        調用了__getattr__,屬性名稱為age
                      #        屬性age的默認值

setattr和delattr

同樣地,您可以控制如何設置和刪除屬性:

  1. __setattr__:當屬性被設置時被調用

  2. __delattr__:當屬性被刪除時被調用

這些方法讓你在修改屬性時實現驗證、日誌記錄或自定義行為。

實用範例:自動日誌記錄屬性

讓我們創建一個自動記錄所有屬性變更的類。這對於除錯、審計或追蹤物件狀態變更非常有用:

import logging

# 設置日誌記錄
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class LoggedObject:
    def __init__(self, **kwargs):
        self._data = {}
        # 在不觸發 __setattr__ 的情況下初始化屬性
        for key, value in kwargs.items():
            self._data[key] = value

    def __getattr__(self, name):
        if name in self._data:
            logging.debug(f"Accessing attribute {name}: {self._data[name]}")
            return self._data[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == "_data":
            # 允許直接設置 _data 屬性
            super().__setattr__(name, value)
        else:
            old_value = self._data.get(name, "<undefined>")
            self._data[name] = value
            logging.info(f"Changed {name}: {old_value} -> {value}")

    def __delattr__(self, name):
        if name in self._data:
            old_value = self._data[name]
            del self._data[name]
            logging.info(f"Deleted {name} (was: {old_value})")
        else:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

讓我們分析這個類是如何運作的:

  1. 存儲:該類使用私有 _data 字典來存儲屬性值。

  2. 屬性訪問

    • __getattr__:從 _data 返回值並記錄調試消息

    • __setattr__:將值存儲到 _data 並記錄變更

    • __delattr__:從 _data 中移除值並記錄刪除

  3. 特殊處理: 為了避免無限遞歸,_data屬性本身會被處理得不同。

以下是如何使用該類:

# 創建帶有初始值的已記錄對象
user = LoggedObject(name="Vivek", email="[email protected]")

# 修改屬性
user.name = "Vivek"  # 記錄:更改名稱:Vivek -> Vivek
user.age = 30         # 記錄:更改年齡:<undefined> -> 30

# 訪問屬性
print(user.name)      # 輸出:Vivek

# 刪除屬性
del user.email        # 記錄:刪除電子郵件(原為:[email protected]

# 嘗試訪問已刪除的屬性
try:
    print(user.email)
except AttributeError as e:
    print(f"AttributeError: {e}")  # 輸出:AttributeError:'LoggedObject'對象沒有屬性'email'

此實現提供了幾個優點:

  1. 自動記錄所有屬性更改

  2. 調試級別的屬性訪問日誌

  3. 缺少屬性的清晰錯誤信息

  4. 輕鬆跟踪對象狀態變化

  5. 用於調試和審計

上下文管理器

上下文管理器是Python中的一個強大功能,可以幫助您正確管理資源。它們確保資源被正確獲取和釋放,即使出現錯誤也是如此。`with`陳述式是使用上下文管理器的最常見方式。

進入和退出

要創建一個上下文管理器,您需要實現兩個特殊方法:

  1. __enter__:進入`with`區塊時調用。應返回要管理的資源。

  2. __exit__:無論是否有異常發生,在退出`with`區塊時調用。應該處理清理工作。

`__exit__`方法接收三個參數:

  • exc_type:異常的類型(如果有)

  • exc_val:異常的實例(如果有)

  • exc_tb:追蹤信息(如果有)

實際範例:資料庫連接管理器

讓我們為資料庫連線建立一個上下文管理器。這個例子展示了如何正確管理資料庫資源並處理交易:

import sqlite3
import logging

# 設置日誌
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class DatabaseConnection:
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
        self.cursor = None

    def __enter__(self):
        logging.info(f"Connecting to database: {self.db_path}")
        self.connection = sqlite3.connect(self.db_path)
        self.cursor = self.connection.cursor()
        return self.cursor

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            logging.error(f"An error occurred: {exc_val}")
            self.connection.rollback()
            logging.info("Transaction rolled back")
        else:
            self.connection.commit()
            logging.info("Transaction committed")

        if self.cursor:
            self.cursor.close()
        if self.connection:
            self.connection.close()

        logging.info("Database connection closed")

        # 返回 False 以傳播異常,返回 True 以忽略異常
        return False

讓我們來分解這個上下文管理器的工作原理:

  1. 初始化:

    • 該類接受一個資料庫路徑

    • 它將連線和游標初始化為 None

  2. 進入方法:

    • 創建一個資料庫連線

    • 創建一個游標

    • 返回用於 with 區塊中使用的游標

  3. 退出方法:

    • 处理事务管理(提交/回滚)

    • 关闭游标和连接

    • 记录所有操作

    • 返回False以传播异常

以下是如何使用上下文管理器:

# 在内存中创建一个测试数据库
try:
    # 成功的事务
    with DatabaseConnection(":memory:") as cursor:
        # 创建表
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)

        # 插入数据
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Vivek", "[email protected]")
        )

        # 查询数据
        cursor.execute("SELECT * FROM users")
        print(cursor.fetchall())  # 输出: [(1, 'Vivek', '[email protected]')]

    # 演示发生错误时的事务回滚
    with DatabaseConnection(":memory:") as cursor:
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Wewake", "[email protected]")
        )
        # 这将导致错误 - 表 'nonexistent' 不存在
        cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
    print(f"Caught exception: {e}")

此上下文管理器提供了几个好处:

  1. 资源会被自动管理(例如:连接始终会被关闭)。

  2. 具有事務安全性,更改會被適當提交或回滾。

  3. 異常情況會被捕獲並優雅地處理

  4. 所有操作都被記錄以進行調試

  5. with語句使代碼清晰而簡潔

可調用對象

__call__魔法方法讓您的類實例表現得像函數一樣。這對於創建在調用之間保持狀態的對象或實現具有額外功能的函數行為非常有用。

call

__call__方法在嘗試調用類實例時被調用,就像它是函數一樣。這裡是一個簡單的例子:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

# 創建像函數一樣的實例
double = Multiplier(2)
triple = Multiplier(3)

print(double(5))  # 輸出:10
print(triple(5))  # 輸出:15

此示例顯示了如何使用__call__創建在調用之間保持狀態(因子)並且可以像函數一樣被調用的對象。

實際示例:記憶化裝飾器

讓我們使用 __call__ 實現一個備忘錄裝飾器。這個裝飾器將緩存函數結果以避免重複計算:

import time
import functools

class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        # 保存函數元數據(名稱、文檔字符串等)
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        # 從參數創建鍵
        # 為了簡化,我們假設所有參數都是可哈希的
        key = str(args) + str(sorted(kwargs.items()))

        if key not in self.cache:
            self.cache[key] = self.func(*args, **kwargs)

        return self.cache[key]

# 用法
@Memoize
def fibonacci(n):
    """Calculate the nth Fibonacci number recursively."""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 測量執行時間
def time_execution(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()
    print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
    return result

# 沒有備忘錄,這將非常緩慢
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

# 第二次調用由於備忘錄而瞬間完成
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

讓我們來拆解這個備忘錄裝飾器是如何工作的:

  1. 初始化

    • 將函數作為參數

    • 創建一個緩存字典來存儲結果

    • 使用 functools.update_wrapper 保存函數的元數據

  2. 呼叫方法:

    • 根據函數參數創建唯一鍵

    • 檢查結果是否在快取中

    • 如果沒有,計算結果並存儲

    • 返回快取結果

  3. 使用方法:

    • 作為裝飾器應用於任何函數

    • 自動快取重複調用的結果

    • 保留函數的元資料和行為

此實現的好處包括:

  1. 更好的性能,因為它避免了冗餘計算

  2. 更好,透明度高,因为它在不修改原始功能的情况下工作

  3. 它很灵活,可以与任何函数一起使用

  4. 它具有高效的内存使用,并对结果进行缓存以供重复使用

  5. 它维护函数文档

高级魔术方法

现在让我们来探索一些Python的更高级的魔术方法。这些方法可以让您对对象的创建、内存使用和字典行为进行精细控制。

用于对象创建的new方法

__new__方法在__init__之前调用,并负责创建和返回类的新实例。这对于实现单例模式或不可变对象等模式非常有用。

下面是使用__new__实现的单例模式的示例:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name=None):
        # 每次调用Singleton()时都会调用此方法
        if name is not None:
            self.name = name

# 使用方法
s1 = Singleton("Vivek")
s2 = Singleton("Wewake")
print(s1 is s2)  # 输出:True
print(s1.name)   # 输出:Wewake(第二次初始化覆盖了第一次)

让我们来解析一下这个单例模式的工作原理:

  1. 類別變數: _instance 儲存類別的單一實例

  2. new 方法:

    • 檢查實例是否存在

    • 如果不存在則創建一個

    • 如果存在則返回現有的實例

  3. init 方法:

    • 每當使用構造函數時被調用

    • 更新實例的屬性

內存優化的 slots

__slots__ 類別變數限制了實例可以擁有的屬性,節省內存。當你擁有許多具有固定屬性集的類別實例時,這特別有用。

這裡是常規類別和槽類別的比較:

import sys

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

# 比較記憶體使用量
regular_people = [RegularPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
slotted_people = [SlottedPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]

print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes")  # 輸出:普通人的大小:48位元組
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes")  # 輸出:槽人的大小:56位元組
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes")  # 輸出:每個實例節省的記憶體量:-8位元組
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB")  # 輸出:1000個實例的總節省記憶體:-7.81 KB

運行此代碼會產生一個有趣的結果:

Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB

令人驚訝的是,在這個簡單的例子中,槽實例實際上比常規實例大8位元組!這似乎與關於__slots__節省記憶體的常見建議相矛盾。

那麼這裡到底發生了什麼?從__slots__中真正節省的記憶體來看:

  1. 消除字典:普通的Python對象將其屬性存儲在一個字典(__dict__)中,這帶有開銷。 sys.getsizeof()函數並不考慮這個字典的大小。

  2. 存儲屬性:對於具有少量屬性的小對象,槽描述符的開銷可能超過字典節省。

  3. 可伸縮性:真正的好處出現在:

    • 您有許多實例(數千或數百萬個)

    • 您的對象具有許多屬性

    • 您正在動態添加屬性

讓我們看一個更完整的比較:

# 更準確的內存測量
import sys

def get_size(obj):
    """Get a better estimate of the object's size in bytes."""
    size = sys.getsizeof(obj)
    if hasattr(obj, '__dict__'):
        size += sys.getsizeof(obj.__dict__)
        # 添加字典內容的大小
        size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
    return size

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

regular = RegularPerson("Vivek", 30, "[email protected]")
slotted = SlottedPerson("Vivek", 30, "[email protected]")

print(f"Complete Regular person size: {get_size(regular)} bytes")  # 輸出: 完整的普通人大小: 610字節
print(f"Complete Slotted person size: {get_size(slotted)} bytes")  # 輸出: 完整的槽人大小: 56字節

通過這種更準確的測量,您將看到,特別是在添加更多屬性時,槽對象通常使用的總記憶體更少。

  1. 關於__slots__的關鍵點:

    真正的記憶體優勢: 主要的記憶體節省來自於消除實例__dict__

  2. 動態限制:您無法將任意屬性添加到插槽對象中

  3. 繼承注意事項:在繼承中使用__slots__需要仔細規劃

  4. 使用情境:適用於具有多個實例和固定屬性的類

  5. 性能加成:在某些情況下還可以提供更快的屬性訪問

缺失的默認字典值

當字典子類找不到鍵時,會調用__missing__方法。這對於實現具有默認值或自動創建鍵的字典非常有用。

這是一個自動為缺失鍵創建空列表的字典的示例:

class AutoKeyDict(dict):
    def __missing__(self, key):
        self[key] = []
        return self[key]

# 用法
groups = AutoKeyDict()
groups["team1"].append("Vivek")
groups["team1"].append("Wewake")
groups["team2"].append("Vibha")

print(groups)  # 輸出:{'team1': ['Vivek', 'Wewake'], 'team2': ['Vibha']}

這種實現提供了幾個好處:

  1. 不需要檢查鍵是否存在,更方便。

  2. 自動初始化根據需要創建默認值。

  3. 減少字典初始化的樣板代碼。

  4. 更靈活,可以實現任何默認值邏輯。

  5. 只在需要時創建值,更節省內存。

性能考慮

魔法方法很強大,但如果不小心使用,可能會影響性能。讓我們探討一些常見的性能考慮因素以及如何進行測量。

魔法方法對性能的影響

不同的魔法方法對性能有不同的影響:

屬性訪問方法:

  • __getattr____getattribute____setattr____delattr__會被頻繁調用

  • 這些方法中的復雜操作可能會大大減慢代碼速度

容器方法:

  • __getitem____setitem____len__ 在循环中被频繁调用

  • 低效的实现可能使您的容器比内置类型慢得多

运算符重载:

  • 算术和比较运算符经常被使用

  • 复杂的实现可能使简单操作意外地变慢

让我们测量 __getattr__ 与直接属性访问的性能影响:

import time

class DirectAccess:
    def __init__(self):
        self.value = 42

class GetAttrAccess:
    def __init__(self):
        self._value = 42

    def __getattr__(self, name):
        if name == "value":
            return self._value
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

# 测量性能
direct = DirectAccess()
getattr_obj = GetAttrAccess()

def benchmark(obj, iterations=1000000):
    start = time.time()
    for _ in range(iterations):
        x = obj.value
    end = time.time()
    return end - start

direct_time = benchmark(direct)
getattr_time = benchmark(getattr_obj)

print(f"Direct access: {direct_time:.6f} seconds")
print(f"__getattr__ access: {getattr_time:.6f} seconds")
print(f"__getattr__ is {getattr_time / direct_time:.2f}x slower")

运行这个基准测试显示了显著的性能差异:

Direct access: 0.027714 seconds
__getattr__ access: 0.060646 seconds
__getattr__ is 2.19x slower

如您所见,使用 __getattr__ 比直接属性访问慢了两倍多。对于偶尔访问的属性可能无关紧要,但在访问属性的紧密循环中变得显著的性能关键代码中可能会变得重要。

优化策略

幸运的是,有各种方式可以优化魔术方法。

  1. 使用 slots 进行内存效率:这减少了内存使用量并提高了属性访问速度。适用于具有许多实例的类。

  2. 快取計算值:您可以儲存昂貴操作的結果,並且僅在必要時更新快取。對於計算屬性,使用@property

  3. 最小化方法調用:確保避免不必要的魔術方法調用,並在可能的情況下使用直接屬性訪問。考慮為經常訪問的屬性使用__slots__

最佳實踐

在使用魔術方法時,遵循這些最佳實踐以確保您的代碼可維護、高效且可靠。

1. 保持一致性

在實現相關的魔術方法時,保持行為的一致性:

from functools import total_ordering

@total_ordering
class ConsistentNumber:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value < other.value

2. 返回 NotImplemented

當操作沒有意義時,返回NotImplemented,讓 Python 嘗試反向操作:

class Money:
    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        # ... 實現的其餘部分

3. 保持簡單

魔法方法應該簡單且可預測。避免複雜的邏輯,以免導致意外行為:

# 好的:簡單且可預測
class SimpleContainer:
    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]

# 不好的:複雜且可能令人困惑
class ComplexContainer:
    def __init__(self):
        self.items = []
        self.access_count = 0

    def __getitem__(self, index):
        self.access_count += 1
        if self.access_count > 100:
            raise RuntimeError("Too many accesses")
        return self.items[index]

4. 記錄行為

清楚記錄你的魔法方法如何運作,特別是當它們偏離標準預期時:

class CustomDict(dict):
    def __missing__(self, key):
        """
        Called when a key is not found in the dictionary.
        Creates a new list for the key and returns it.
        This allows for automatic list creation when accessing
        non-existent keys.
        """
        self[key] = []
        return self[key]

5. 考慮性能

注意性能影響,尤其是對於經常被調用的方法:

class OptimizedContainer:
    __slots__ = ['items']  # 使用 __slots__ 以獲得更好的性能

    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]  # 直接訪問更快

6. 處理邊界情況

始終考慮邊界情況並妥善處理:

class SafeContainer:
    def __getitem__(self, key):
        if not isinstance(key, (int, slice)):
            raise TypeError("Index must be integer or slice")
        if key < 0:
            raise ValueError("Index cannot be negative")
        # ... 其餘實現

總結

Python 的魔法方法提供了一種強大的方式,使你的類別表現得像內建類型,實現更直觀和表達力的代碼。在本指南中,我們探討了這些方法的工作原理以及如何有效使用它們。

主要要點

  1. 對象表示:

    • 使用 __str__ 來獲得用戶友好的輸出

    • 使用 __repr__ 來進行調試和開發

  2. 運算符重載

    • 實現算術和比較運算符

    • 對不支持的操作返回NotImplemented

    • 使用@total_ordering進行一致的比較

  3. 容器行為

    • 實現序列和映射協議

    • 考慮頻繁使用的操作的性能

    • 適當處理邊界情況

  4. 资源管理:

    • 使用上下文管理器来正确处理资源

    • 实现__enter____exit__进行清理

    • __exit__中处理异常

  5. 性能优化:

    • 使用__slots__进行内存效率优化

    • 适当时缓存计算值

    • 在频繁使用的代码中尽量减少方法调用

何时使用魔术方法

当您需要时,魔术方法非常有用:

  1. 創建自定義數據結構

  2. 實現特定領域的類型

  3. 妥善管理資源

  4. 為你的類添加特殊行為

  5. 使你的代碼更具 Python 風格

何時避免使用魔術方法

當以下情況發生時,應避免使用魔術方法:

  1. 簡單的屬性訪問就足夠了

  2. 行為會令人困惑或出乎意料

  3. 性能至關重要,而魔術方法會增加開銷

  4. 實現過於複雜

記住,能力越大,責任越大。明智地使用魔術方法,考慮其性能影響和最小驚訝原則。當適當使用時,魔術方法可以顯著提高你的代碼的可讀性和表達性。

參考資料及進一步閱讀

官方 Python 文檔

  1. Python 數據模型 – 官方文檔 – Python 數據模型和魔法方法的綜合指南。

  2. functools.total_ordering – 自動填充缺失比較方法的 total_ordering 裝飾器的文檔。

  3. Python 特殊方法名稱 – Python 中特殊方法標識符的官方參考。

  4. 集合抽象基類 – 學習有關容器的抽象基類,這些基類定義了您的容器類可以實現的接口。

社區資源

  1. Python 魔術方法指南 – Rafe Kettler – 魔術方法和常見用例的實際範例。

進一步閱讀

如果您喜歡這篇文章,您可能會發現我的個人部落格上這些與 Python 相關的文章有用:

  1. Django ORM 查詢優化的實際實驗 – 通過實際範例和實驗來優化您的 Django ORM 查詢。

  2. 同步uWSGI的高成本 – 了解在uWSGI中同步处理的性能影响以及它对Python网页应用的影响。