在 Python 中,三個關於可哈希不可不知的問題

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!


作爲一種通用的編程語言,Python 爲不同的用戶場景提供了一系列內置的數據結構。

當你學習 Python 基礎知識的時候,你可能在某些地方看到有提及可哈希。例如,你可能會看到 dict 中的鍵需要是可哈希的(請參見下面代碼片段中的一個小示例)。

在另一個例子中,它提到了 set 中的元素需要是可哈希的。

>>> # 一個正確的字典聲明
>>> good_dict = {"a": 1, "b": 2}
>>> 
>>> # 一個錯誤的字典聲明
>>> failed_dict = {["a"]: 1, ["b"]: 2}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

你可能會想知道 —— 可哈希到底是什麼意思?哪些對象是可哈希的,而哪些又不是?如果我們使用不可哈希的對象作爲字典的鍵會發生什麼?諸如此類,你可能還想問很多相關的問題。

在本文中,我們將討論關於哈希的一些基本的知識點,以讓你可以解決上述的這些問題。最後你可能會發現這些問題一點都不像你開始想的那麼難。

哪些對象是可哈希的,而哪些又不是?

當我們展開關於哈希的機制解釋之前,第一個需要解決的問題就是哪些對象是可哈希的,而哪些又不是。

因爲我們知道 Python 顯式的要求能夠加入 set 的元素應該是可哈希的,所以我們可以通過測試是否可以將一個對象加入 set 來判斷它的哈希屬性。成功插入表示對象是可以被哈希的,反之亦然。

>>> # 創建一個空的集合
>>> elements = set()
>>> 
>>> # 將各種類型的對象插入到這個集合中
>>> items = [1, 0.1, 'ab', (2, 3), {'a': 1}, [1, 2], {2, 4}, None]

如上述代碼所示,我創建了一個命名爲 elements 的 set 變量,以及另一個命名爲 items 的 list 變量,items 中包含了最常用的內置數據類型:int、float、str、tuple、dict、list、set 和 NoneType。

我將進行的實驗是將 items 中的每一個元素添加到 elements 中。在這個場景下我不會使用 for 循環,因爲任何一個可能的 TypeError 都會導致迭代的中止。相反,我們會是根據索引來迭代每一項。

>>> elements.add(items[0])
>>> elements.add(items[1])
>>> elements.add(items[2])
>>> elements.add(items[3])
>>> elements.add(items[4])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> elements.add(items[5])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> elements.add(items[6])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
>>> elements.add(items[7])
>>> elements
{0.1, 1, 'abc', None, (2, 3)}

正如你在如上代碼片段所看到的,這裏對實驗結果做出了一個簡單的總結。

對本節問題的回答

  • 可以被哈希的數據結構:int、float、str、tuple 和 NoneType。
  • 不可以被哈希的數據結構:dict、list 和 set。

即使你對 Python 編程還不太熟悉,但你可能也已經注意到這三種不可哈希的數據結構在實際上都是可變的,而這五種可哈希的數據結構都是不可變的。

本質上,這些可變的數據結構是指其這個對象在創建後可以被更改,而不可變對象的值在創建後不能被更改。

可哈希意味着什麼?

現在你已經知道哪些對象是可以被哈希的,哪些不是,但是哈希到底是什麼意思呢?

實際上,你可能聽說過許多類似的計算機術語都與哈希相關,如哈希值、哈希化、哈希表和哈希圖。它們的核心在於它們有着相同的基本過程 —— 哈希化。

5

上圖展示了哈希化的一般過程。我們從一些原始數據值(圖中稱爲 key)開始。

哈希函數有時被稱爲哈希器,它將執行特定的計算並輸出原始數據值的哈希值(在圖中稱爲 hashes)。

哈希化及其相關概念需要一整本書來闡述,那超出了本文的範圍。不過,我在我的 上一篇文章 中簡要討論了一些重要方面。

在這裏,我將強調與本次討論相關的一些要點。

  • 哈希函數應該是計算健壯的,以使不同的對象得到不同的哈希值。當不同的對象具有相同的哈希值時就會發生衝突(如上圖所示),並應該進行處理。
  • 哈希函數還應該是具有一致性,以使相同的對象將始終得到相同的哈希值。

Python 已經實現了內置哈希函數來爲它的對象生成哈希值。具體來說,我們可以使用內置的 hash() 函數來找到對象的哈希值。下面的代碼將向你展示一些示例。

>>> # 得到一個字符串對象的哈希值
>>> hash("Hello World!")
5892830027481816904
>>> 
>>> # 得到一個元組對象的哈希值
>>> hash((2, 'Hello'))
-4798835419149360612
>>> 
>>> # 得到一個列表對象的哈希值
>>> hash([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> 
>>> # 得到一個字典對象的哈希值
>>> hash({"a": 1, "b": 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'

正如上述代碼所示,我們可以爲 int 和 tuple 類型得到哈希值 —— 整數串。

但是,list 對象和 dict 對象都沒有哈希值,這些結果與我們在上一節中對可哈希對象和不可哈希對象所做的區分是一致的。

對本節問題的回答

是否可哈希:Python 對象的一個特性,用於判斷對象是否具有哈希值,這將是對象充當字典鍵或集合中元素的必要條件。

我們可以如何定製哈希屬性?

Python 作爲通用編程語言的靈活性主要來自於它對創建自定義類的支持。有了自己的類,許多相關的數據和操作可以用更有意義和可讀性的方式歸類。

而且重要的是,Python 已經逐步發展到足夠聰明,可以在大多數情況下使我們的自定義對象默認是可哈希的。

正如下面的例子。我們創建了一個自定義類 Person,它允許我們通過指定一個人的姓名和社會保險號碼來創建實例。

同時值得注意的是,我們使用 f-string 方法重寫了默認的 __repr__() 函數,這將允許我們用更加可讀的信息顯示對象,就像代碼最後一行所展示的那樣。

>>> # 創建一個自定義類
>>> class Person:
...     def __init__(self, name, ssn):
...         self.name = name
...         self.ssn = ssn
...
...     def __repr__(self):
...         return f"***Name: {self.name}; SSN: {self.ssn}***"
... 
>>> # 創建一個自定義實例並檢查哈希值
>>> person0 = Person('John Smith', '012345678')
>>> hash(person0)
272583189
>>> 
>>> # 創建一個包含這個 Person 對象的集合
>>> persons = {person0}
>>> persons
{***Name: John Smith; SSN: 012345678***}

如上面的代碼所示,我們可以使用內置的 hash() 函數來查找創建的對象 person0 的哈希值。而且重要的是,我們可以將 person0 對象作爲元素包含在 set 對象中,這很棒哦。

但是,如果我們想向集合中添加更多的 Person 實例,會發生什麼情況呢?一個更復雜但可能的場景是,我們構造同一個人的多個 Person 對象,並嘗試將它們添加到 set 對象中。

請參閱如下代碼。我創建了另一個 Person 實例 person1,它具有相同的名稱和社會保險號碼 — 本質上是相同的自然人。

>>> # 創建了另外一個相同的 Person 對象
>>> person1 = Person('John Smith', '012345678')
>>> 
>>> # 將這個 person1 加入到集合中
>>> persons.add(person1)
>>> persons
{***Name: John Smith; SSN: 012345678***, ***Name: John Smith; SSN: 012345678***}
>>> 
>>> # 比較兩個 Person 對象
>>> person0 == person1
False

但是,當我們將這個新的對象添加到 set 對象 persons 時,兩個 Person 對象卻都包含在 set 中,這是我們不希望發生的。

因爲根據設計,我們希望 set 對象存儲的自然人都不重複。通過比較 set 對象存儲的每一個 Person 對象,我們需要發現它們都是不相同的。

我將向你展示如何使自定義類 Person 更智能,以便它知道哪些自然人對象是相同的,哪些是不同的。

>>> # 優化 Person 類函數
>>> class Person:
...     # __init__ and __repr__ stay the same
...
...     def __hash__(self):
...         print("__hash__ is called")
...         return hash((self.name, self.ssn))
...
...     def __eq__(self, other):
...         print("__eq__ is called")
...         return (
...             self.__class__ == other.__class__ and 
...             self.name == other.name and
...             self.ssn == other.ssn
...         )
...
>>> # 創建兩個 Person 對象
>>> p0 = Person("Jennifer Richardson", 123456789)
>>> p1 = Person("Jennifer Richardson", 123456789)
>>> 
>>> # 創建一個集合,幷包含這兩個對象
>>> ps = {p0, p1}
__hash__ is called
__hash__ is called
__eq__ is called
>>> ps
{***Name: Jennifer Richardson; SSN: 123456789***}
>>> 
>>> # 比較這兩個對象
>>> p0 == p1
__eq__ is called
True

在上述代碼中,我們通過重寫 hasheq 函數更新了自定義類 Person。

我們之前提到過,__hash__() 函數是用於計算對象的哈希值。而 __eq__() 函數用於比較對象與另一個對象是否相等,並且要求比較後相等的對象應該具有 相同的哈希值。

默認情況下,自定義類的實例是通過使用內置的 id() 函數獲取的標識進行比較的(如果需要了解更多相關 id() 函數的內容請參閱 本文)。

通過更新後的實現,我們可以看到當我們試圖創建包含兩個相同 Person 對象的 set 對象時,__hash__() 會被調用進行判斷,然後集合對象中僅會保留具有唯一哈希值的對象。

另一件需要注意的事情是,當 Python 檢查 set 對象中的元素是否具有唯一哈希值時,它將通過調用 __eq__() 函數來確保這些對象不相等。

對本節問題的回答

定製化:爲了提供定製的哈希和比較策略,我們需要在我們的定製類中實現 hasheq 函數。

總結

在本文中,我們回顧了 Python 裏面可哈希和哈希化的概念。

具體來說,通過解決這三個重要問題,我希望你能夠更好地理解 Python 中的哈希。當在適用的場景,你可以爲自己的自定義類實現定製的哈希行爲。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-06-24
本文作者:cyril_lee
本文來自:“掘金”,瞭解相關信息可以關注“掘金”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章