一篇來自 Dan Bader 的有趣的博文,一塊兒來學習一下,如何去研究一個意外的Python現象。html
讓咱們探究一下下面這個晦澀的python字典表達式,以找出在python解釋器的中未知的內部到底發生了什麼。python
# 一個python謎題:這是一個祕密
# 這個表達式計算之後會獲得什麼結果?
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
複製代碼
有時候你會碰到一個頗有深度的代碼示例 --- 哪怕僅僅是一行代碼,可是若是你可以有足夠的思考,它能夠教會你不少關於編程語言的知識。這樣一個代碼片斷,就像是一個*Zen kōan
*:一個在修行的過程當中用來質疑和考驗學生進步的問題或陳述。express
譯者注:Zen kōan
,大概就是修行的一種方式,詳情見wikipedia編程
咱們將在本教程中討論的小代碼片斷就是這樣一個例子。乍看之下,它可能看起來像一個簡單的詞典表達式,可是仔細考慮時,經過cpython解釋器,它會帶你進行一次思惟拓展的訓練。vim
我從這個短短的一行代碼中獲得了一個啓發,並且有一次在我參加的一個Python會議上,我還把做爲我演講的內容,並以此開始演講。這也激發了個人python郵件列表成員間進行了一些積極的交流。性能優化
因此不用多說,就是這個代碼片。花點時間思考一下下面的字典表達式,以及它計算後將獲得的內容:數據結構
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
複製代碼
在這裏,我先等會兒,你們思考一下...編程語言
OK, 好了嗎?函數
這是在cpython解釋器交互界面中計算上述字典表達式時獲得的結果:工具
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
複製代碼
我認可,當我第一次看到這個結果時,我很驚訝。可是當你逐步研究其中發生的過程時,這一切都是有道理的。因此,讓咱們思考一下爲何咱們獲得這個 - 我想說的是出乎意料 - 的結果。
當python處理咱們的字典表達式時,它首先構造一個新的空字典對象;而後按照字典表達式給出的順序賦鍵和值。
所以,當咱們把它分解開的時候,咱們的字典表達就至關於這個順序的語句:
>>> xs = dict()
>>> xs[True] = 'yes'
>>> xs[1] = 'no'
>>> xs[1.0] = 'maybe'
複製代碼
奇怪的是,Python認爲在這個例子中使用的全部字典鍵是相等的:
>>> True == 1 == 1.0
True
複製代碼
OK,但在這裏等一下。我肯定你可以接受1.0 == 1,但實際狀況是爲何True
也會被認爲等於1呢?我第一次看到這個字典表達式真的讓我難住了。
在python文檔中進行一些探索以後,我發現python將bool
做爲了int
類型的一個子類。這是在Python 2和Python 3的片斷:
「The Boolean type is a subtype of the integer type, and Boolean values behave like the values 0 and 1, respectively, in almost all contexts, the exception being that when converted to a string, the strings ‘False’ or ‘True’ are returned, respectively.」
「布爾類型是整數類型的一個子類型,在幾乎全部的上下文環境中布爾值的行爲相似於值0和1,例外的是當轉換爲字符串時,會分別將字符串」False「或」True「返回。「(原文)
是的,這意味着你能夠在編程時上使用bool
值做爲Python中的列表或元組的索引:
>>> ['no', 'yes'][True]
'yes'
複製代碼
但爲了代碼的可讀性起見,您不該該相似這樣的來使用布爾變量。(也請建議你的同事別這樣作)
Anyway,讓咱們回過來看咱們的字典表達式。
就python而言,True
,1
和1.0
都表示相同的字典鍵。當解釋器計算字典表達式時,它會重複覆蓋鍵True
的值。這就解釋了爲何最終產生的字典只包含一個鍵。
在咱們繼續以前,讓咱們再回顧一下原始字典表達式:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
複製代碼
這裏爲何最終獲得的結果是以True
做爲鍵呢?因爲重複的賦值,最後不該該是把鍵也改成1.0
了?通過對cpython解釋器源代碼的一些模式研究,我知道了,當一個新的值與字典的鍵關聯的時候,python的字典不會更新鍵對象自己:
>>> ys = {1.0: 'no'}
>>> ys[True] = 'yes'
>>> ys
{1.0: 'yes'}
複製代碼
固然這個做爲性能優化來講是有意義的 --- 若是鍵被認爲是相同的,那麼爲何要花時間更新原來的?在最開始的例子中,你也能夠看到最初的True
對象一直都沒有被替換。所以,字典的字符串表示仍然打印爲以True
爲鍵(而不是1或1.0)。
就目前咱們所知而言,彷佛看起來像是,結果中字典的值一直被覆蓋,只是由於他們的鍵比較後相等。然而,事實上,這個結果也不僅僅是由__eq__
比較後相等就得出的。
python字典類型是由一個哈希表數據結構存儲的。當我第一次看到這個使人驚訝的字典表達式時,個人直覺是這個結果與散列衝突有關。
哈希表中鍵的存儲是根據每一個鍵的哈希值的不一樣,包含在不一樣的「buckets」中。哈希值是指根據每一個字典的鍵生成的一個固定長度的數字串,用來標識每一個不一樣的鍵。(哈希函數詳情)
這能夠實現快速查找。在哈希表中搜索鍵對應的哈希數字串會快不少,而不是將完整的鍵對象與全部其餘鍵進行比較,來檢查互異性。
然而,一般計算哈希值的方式並不完美。而且,實際上會出現不一樣的兩個或更多個鍵會生成相同的哈希值,而且它們最後會出如今相同的哈希表中。
若是兩個鍵具備相同的哈希值,那就稱爲哈希衝突(hash collision),這是在哈希表插入和查找元素時須要處理的特殊狀況。
基於這個結論,哈希值與咱們從字典表達中獲得的使人意外的結果有很大關係。因此讓咱們來看看鍵的哈希值是否也在這裏起做用。
我定義了這樣一個類來做爲咱們的測試工具:
class AlwaysEquals:
def __eq__(self, other):
return True
def __hash__(self):
return id(self)
複製代碼
這個類有兩個特別之處。
第一,由於它的__eq__
魔術方法(譯者注:雙下劃線開頭雙下劃線結尾的是一些Python的「魔術」對象)老是返回true,因此這個類的全部實例和其餘任何對象都會恆等:
>>> AlwaysEquals() == AlwaysEquals()
True
>>> AlwaysEquals() == 42
True
>>> AlwaysEquals() == 'waaat?'
True
複製代碼
第二,每一個Alwaysequals
實例也將返回由內置函數id()
生成的惟一哈希值值:
>>> objects = [AlwaysEquals(),
AlwaysEquals(),
AlwaysEquals()]
>>> [hash(obj) for obj in objects]
[4574298968, 4574287912, 4574287072]
複製代碼
在CPython中,id()
函數返回的是一個對象在內存中的地址,而且是肯定惟一的。
經過這個類,咱們如今能夠建立看上去與其餘任何對象相同的對象,但它們都具備不一樣的哈希值。咱們就能夠經過這個來測試字典的鍵是不是基於它們的相等性比較結果來覆蓋。
正如你所看到的,下面的一個例子中的鍵不會被覆蓋,即便它們老是相等的:
>>> {AlwaysEquals(): 'yes', AlwaysEquals(): 'no'}
{ <AlwaysEquals object at 0x110a3c588>: 'yes',
<AlwaysEquals object at 0x110a3cf98>: 'no' }
複製代碼
下面,咱們能夠換個思路,若是返回相同的哈希值是否是就會讓鍵被覆蓋呢?
class SameHash:
def __hash__(self):
return 1
複製代碼
這個SameHash
類的實例將相互比較必定不相等,但它們會擁有相同的哈希值1:
>>> a = SameHash()
>>> b = SameHash()
>>> a == b
False
>>> hash(a), hash(b)
(1, 1)
複製代碼
一塊兒來看看python的字典在咱們試圖使用SameHash
類的實例做爲字典鍵時的結果:
>>> {a: 'a', b: 'b'}
{ <SameHash instance at 0x7f7159020cb0>: 'a',
<SameHash instance at 0x7f7159020cf8>: 'b' }
複製代碼
如本例所示,「鍵被覆蓋」的結果也並非單獨由哈希衝突引發的。
python字典類型是檢查兩個對象是否相等,並比較哈希值以肯定兩個密鑰是否相同。讓咱們試着總結一下咱們研究的結果:
{true:'yes',1:'no',1.0:'maybe'}
字典表達式計算結果爲{true:'maybe'}
,是由於鍵true
,1
和1.0
都是相等的,而且它們都有相同的哈希值:
>>> True == 1 == 1.0
True
>>> (hash(True), hash(1), hash(1.0))
(1, 1, 1)
複製代碼
也許並不那麼使人驚訝,這就是咱們爲什麼獲得這個結果做爲字典的最終結果的緣由:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
複製代碼
咱們在這裏涉及了不少方面內容,而這個特殊的python技巧起初可能有點使人難以置信 --- 因此我一開始就把它比做是Zen kōan
。
若是很難理解本文中的內容,請嘗試在Python交互環境中逐個去檢驗一下代碼示例。你會收穫一些關於python深處知識。
注:轉載請保留下面的內容