深入理解 python 虛擬機器器:花裡胡哨的魔術方法

2023-05-24 06:01:10

深入理解 python 虛擬機器器:花裡胡哨的魔術方法

在本篇文章當中主要給大家介紹在 cpython 當中一些比較花裡胡哨的魔術方法,以幫助我們自己實現比較花哨的功能,當然這其中也包含一些也非常實用的魔術方法。

深入分析 hash 方法

在 Python 中,__hash__() 方法是一種特殊方法(也稱為魔術方法或雙下劃線方法),用於返回物件的雜湊值。雜湊值是一個整數,用於在字典(dict)和集合(set)等資料結構中進行快速查詢和比較。__hash__() 方法在建立自定義的可雜湊物件時非常有用,例如自定義類的範例,以便可以將這些物件用作字典的鍵或集合的元素。

下面是一些需要注意的問題和範例來幫助理解 __hash__() 方法:

  • 如果兩個物件相等(根據 __eq__() 方法的定義),它們的雜湊值應該相等。即,如果 a == b 為真,則 hash(a) == hash(b) 也為真,這一點非常重要,因為我們在使用集合和字典的時候,就需要保證容器當中每種物件只能夠有一個,如果不滿足這個條還的話,那麼就可能會導致同一種物件在容器當中會存在多個。
  • 重寫 __hash__() 方法通常需要同時重寫 __eq__() 方法,以確保物件的相等性和雜湊值的一致性。
  • 如果物件沒有定義 __eq__方法,那麼也不要定義 __hash__方法,因為如果遇到雜湊值相等的物件時候,如果無法對兩個物件進行比較的話,那麼也會導致容易當中有多個相同的物件。
import random


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

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __hash__(self):
        return hash((self.name, self.age)) + random.randint(0, 1024)

    def __repr__(self):
        return f"[name={self.name}, age={self.age}]"


person1 = Person("Alice", 25)
person2 = Person("Alice", 25)


print(hash(person1))  
print(hash(person2))  


container = set()
container.add(person1)
container.add(person2)
print(container)

在上面程式碼當中我們重寫了 __hash__ 函數,但是物件的雜湊值每次呼叫的時候我們都加入一個亂數,因此即使 name 和 age 都相等,如果 hash 值不想等,那麼可能會造成容器當中存在多個相同的物件,上面的程式碼就會造成相同的物件,上面的程式輸出結果如下所示:

1930083569156318318
1930083569156318292
{[name=Alice, age=25], [name=Alice, age=25]}

如果重寫上面的類物件:

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

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __hash__(self):
        return hash((self.name, self.age))

    def __repr__(self):
        return f"[name={self.name}, age={self.age}]"

那麼容器器當中只會有一個物件。

如果我們只重寫了 __hash__方法的時候也會造成容器當中有多個相同的物件。

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

    # def __eq__(self, other):
    #     return self.name == other.name and self.age == other.age

    def __hash__(self):
        return hash((self.name, self.age)) # + random.randint(0, 1024)

    def __repr__(self):
        return f"[name={self.name}, age={self.age}]"

這是因為如果雜湊值相同的時候還需要在比較兩個物件是否相等,如果相等那麼就不需要將這個物件儲存到容器當中,如果不相等那麼將會將這個物件加入到容器當中。

bool 方法

在 Python 中,object.__bool__() 方法是一種特殊方法,用於定義物件的布林值。它在使用布林運運算元(如 if 語句和邏輯運算)時自動呼叫。__bool__() 方法應該返回一個布林值,表示物件的真值。如果 __bool__() 方法未定義,Python 將嘗試呼叫 __len__() 方法來確定物件的真值。如果 __len__() 方法返回零,則物件被視為假;否則,物件被視為真。

下面是一些需要注意的事項來幫助理解 __bool__() 方法:

  • __bool__() 方法在物件被應用布林運算時自動呼叫。例如,在 if 語句中,物件的真值由 __bool__() 方法確定。
  • __bool__() 方法應該返回一個布林值(TrueFalse)。
  • 如果 __bool__() 方法未定義,Python 將嘗試呼叫 __len__() 方法來確定物件的真值。
  • 當物件的長度為零時,即 __len__() 方法返回零,物件被視為假;否則,物件被視為真。
  • 如果既未定義 __bool__() 方法,也未定義 __len__() 方法,則物件預設為真。

下面是一個範例,展示瞭如何在自定義類中使用 __bool__() 方法:

class NonEmptyList:
    def __init__(self, items):
        self.items = items
    
    def __bool__(self):
        return len(self.items) > 0

my_list = NonEmptyList([1, 2, 3])

if my_list:
    print("The list is not empty.")
else:
    print("The list is empty.")

物件的屬性存取

在Python中,我們可以通過一些特殊方法來客製化屬性存取的行為。本文將深入介紹這些特殊方法,包括__getitem__()__setitem__()__delitem__()__getattr__()方法,以幫助更好地理解屬性存取的機制和應用場景。

__getitem__()方法是用於索引操作的特殊方法。當我們通過索引存取物件的屬性時,Python會自動呼叫該方法,並傳入索引值作為引數。我們可以在該方法中實現對屬性的獲取操作,並返回相應的值。

class MyList:
    def __init__(self):
        self.data = []
    
    def __getitem__(self, index):
        return self.data[index]

my_list = MyList()
my_list.data = [1, 2, 3]

print(my_list[1])  # 輸出: 2

在上面的例子中,我們定義了一個名為MyList的類,它具有一個屬性data,該屬性是一個列表。通過重寫__getitem__()方法,我們使得可以通過索引來存取MyList物件的data屬性。當我們使用my_list[1]的形式進行索引操作時,Python會自動呼叫__getitem__()方法,並將索引值1作為引數傳遞給該方法。

__setitem__()方法用於屬性的設定操作,即通過索引為物件的屬性賦值。當我們使用索引操作並賦值給物件的屬性時,Python會自動呼叫__setitem__()方法,並傳入索引值和賦值的值作為引數。

class MyList:
    def __init__(self):
        self.data = [0 for i in range(2)]

    def __setitem__(self, index, value):
        self.data[index] = value


my_list = MyList()

my_list[0] = 1
my_list[1] = 2

print(my_list.data)  # 輸出: [1, 2]

在上述範例中,我們重寫了__setitem__()方法來實現對物件屬性的設定操作。當我們執行my_list[0] = 1和my_list[1] = 2的賦值操作時,Python會自動呼叫__setitem__()方法,並將索引值和賦值的值傳遞給該方法。在__setitem__()方法中,我們將值賦給了物件的data屬性的相應索引位置。

__delitem__()方法用於刪除物件屬性的特殊方法。當我們使用del語句刪除物件屬性時,Python會自動呼叫__delitem__()方法,並傳入要刪除的屬性的索引值作為引數。

class MyDict:
    
    def __init__(self):
        self.data = dict()

    def __delitem__(self, key):
        print("In __delitem__")
        del self.data[key]


obj = MyDict()
obj.data["key"] = "val"
del obj["key"] # 輸出 In __delitem__

__getattr__() 是一個特殊方法,用於在存取不存在的屬性時自動呼叫。它接收一個引數,即屬性名,然後返回相應的值或引發 AttributeError 異常。

class MyClass:
    def __getattr__(self, name):
        if name == 'color':
            return 'blue'
        else:
            raise AttributeError(f"'MyClass' object has no attribute '{name}'")

my_obj = MyClass()
print(my_obj.color)  # 輸出: blue
print(my_obj.size)   # 引發 AttributeError: 'MyClass' object has no attribute 'size'

在上面的範例中,當存取 my_obj.color 時,由於 color 屬性不存在,Python 會自動呼叫 __getattr__() 方法,並返回預定義的值 'blue'。而當存取 my_obj.size 時,由於該屬性也不存在,__getattr__() 方法會引發 AttributeError 異常。

__setattr__() 是一個特殊方法,用於在設定屬性值時自動呼叫。它接收兩個引數,即屬性名和屬性值。我們可以在該方法中對屬性進行處理、驗證或記錄。

class MyClass:
    def __init__(self):
        self.color = 'red' # 輸出:Setting attribute 'color' to 'red'

    def __setattr__(self, name, value):
        print(f"Setting attribute '{name}' to '{value}'")
        super().__setattr__(name, value)


my_obj = MyClass()
my_obj.color = 'blue'  # 輸出: Setting attribute 'color' to 'blue'

當我們使用 . 的方式去存取物件屬性的時候,首先會呼叫物件的 __getattribute__ 函數,如果屬性不存在才會呼叫 __getattr__。當 __getattribute__ 方法無法找到指定的屬性時,Python 會呼叫 __getattr__ 方法。以下是在之前的範例類 CustomClass 上新增 __getattr__ 方法的程式碼:

class CustomClass:
    def __init__(self):
        self.attribute = "Hello, world!"

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

    def __getattr__(self, name):
        print(f"Attribute {name} not found")
        return None

在這個範例中,我們在 CustomClass 中新增了 __getattr__ 方法。當 __getattribute__ 方法無法找到指定的屬性時,會自動呼叫 __getattr__ 方法,並列印出屬性名稱 "attribute" 以及未找到屬性的提示資訊。

我們執行下面的程式碼:

obj = CustomClass()
print(obj.attribute)
print(obj.nonexistent_attribute)

輸出結果如下所示:

Accessing attribute: attribute
Hello, world!
Accessing attribute: nonexistent_attribute
Attribute nonexistent_attribute not found
None

首先,我們存取存在的屬性 attribute,此時 __getattribute__ 方法被呼叫,並列印出屬性名稱 "attribute",然後返回屬性的實際值 "Hello, world!"。接著,我們嘗試存取不存在的屬性 nonexistent_attribute,由於 __getattribute__ 方法無法找到該屬性,因此會呼叫 __getattr__ 方法,並列印出屬性名稱 "nonexistent_attribute" 以及未找到屬性的提示資訊,然後返回 None

上下文管理器

當我們需要在特定的程式碼塊執行前後進行一些操作時,上下文管理器是一種非常有用的工具。上下文管理器可以確保資源的正確分配和釋放,無論程式碼塊是否出現異常。在Python中,我們可以通過實現 __enter____exit__ 方法來建立自定義的上下文管理器。

下面是一個簡單的上下文管理器範例,展示瞭如何使用 object.__enter__object.__exit__ 方法來建立一個檔案操作的上下文管理器:

class FileContextManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        self.file.close()

with FileContextManager('example.txt', 'w') as file:
    file.write('Hello, world!')

在上述範例中,FileContextManager 類實現了 __enter____exit__ 方法。在 __enter__ 方法中,我們開啟檔案並返回檔案物件,這樣在 with 語句塊中就可以使用該檔案物件。在 __exit__ 方法中,我們關閉檔案。

無論程式碼塊是否丟擲異常,__exit__ 方法都會被呼叫來確保檔案被正確關閉。這樣可以避免資源洩露和檔案鎖定等問題。使用上下文管理器可以簡化程式碼,並提供一致的資源管理方式,特別適用於需要開啟和關閉資源的情況,如檔案操作、資料庫連線等。

上述上下文管理器的 __exit__ 方法有三個引數:exc_typeexc_valuetraceback。下面是對這些引數的詳細介紹:

  • exc_type(異常型別):這個參數列示引發的異常的型別。如果在上下文管理器的程式碼塊中沒有引發異常,它的值將為 None。如果有異常被引發,exc_type 將是引發異常的型別。

  • exc_value(異常值):這個參數列示引發的異常的範例。它包含了關於異常的詳細資訊,如錯誤訊息。如果沒有異常被引發,它的值也將為 None

  • traceback(回溯資訊):這個引數是一個回溯物件,它包含了關於異常的堆疊跟蹤資訊。它提供了導致異常的程式碼路徑和呼叫關係。如果沒有異常被引發,它的值將為 None

總結

在本篇文章當中主要給大家介紹了一些常用和比較重要的魔術方法,這些方法在我們平時用的可能也比較多,比如 hash 和 eq 還有物件的屬性存取,為了方便例外處理可以使用 exit 和 enter 這個方法,其實還有很多其他的魔術方法,這些方法在 python 官網都有介紹,可以直接存取 https://docs.python.org/3/reference/datamodel.html


本篇文章是深入理解 python 虛擬機器器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可存取專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。