怎樣合并字典最符合Python語言習慣?

TraEMVU 8年前發布 | 10K 次閱讀 Python Python開發

來自: http://www.codingpy.com/article/the-idiomatic-way-to-merge-dicts-in-python/

這篇教程探討了哪種合并字典的方式才是最符合Python語言習慣的(idiomatic)。首發于微信公眾號“編程派”,閱讀最新Python教程,請關注編程派。

你有沒有想過在Python中合并兩個或以上字典?

有很多種方法可以解決這個問題:有些比較拙劣,有些不準確,而且大部分都要許多行代碼。

接下來我們一一介紹解決這個問題的不同方法,一起探討到底哪一種是最Pythonic的。

我們的問題

在討論解決方案之前,我們需要明確定義問題。

我們的代碼中有兩個字典: user 和 defaults 。我們希望將二者合并至一個叫 context 的新字典里。

需要滿足以下要求:

  1. 如果存在重復的鍵, user 字典中的值應覆蓋 defaults 字典中的值;
  2. defaults 和 user 中的鍵可以是任意合法的鍵;
  3. defaults 和 user 中的值可以是任意值;
  4. 在創建 context 字典時, defaults 和 user 的元素不能出現變化;
  5. 更新 context 字典時,不能更改 defaults 或 user 字典。

注意:對于第五個要求,我們關注的是對字典的更新,而不是其中包含的對象。如果擔心字典中嵌套對象的可變性,我們可以考慮使用copy.deepcopy。

基本上,我們希望實現下面的操作:

>>> user = {'name': "Trey", 'website': "http://treyhunner.com"}
>>> defaults = {'name': "Anonymous User", 'page_name': "Profile Page"}
>>> context = merge_dicts(defaults, user)  # magical merge function
>>> context
{'website': 'http://treyhunner.com', 'name': 'Trey', 'page_name': 'Profile Page'}

我們還要考慮解決方法是否Pythonic。但是這又是非常主觀的。下面是我們使用的一些評判標準:

  • 解決方法應該簡潔,但不簡短;
  • 解決方法應該可讀,但不過度冗長;
  • 可能的話,解決方法應該為一行代碼,需要的話可以內聯化(written inline);
  • 解決方法的效率不應該太低。

可能的解決方法

既然定義完了需要解決的問題,接下來我們探討下都有哪些解決方法,并分析其中哪個最準確,哪個最符合Python語言習慣。

多次更新(multiple_update)

下面是一種最簡單的合并字典的方式:

context = {}
context.update(defaults)
context.update(user)

這里我們創建了一個新的空字典,并使用其 update 方法從其他字典中添加元素。請注意,我們首先添加的是 defaults 字典中的元素,以保證 user 字典中的重復鍵會覆蓋掉 defaults 中的鍵。

它滿足了全部5個要求,所以這個方法是準確的。它總共有3行代碼,不能內聯執行,但是代碼很清晰。

得分:

  • 準確:是。
  • 符合語言習慣:比較符合,如果能夠內聯執行的話就更好了

復制,然后更新(copy and update)

另外,我們可以復制 defaults 字典,然后使用 user 來更新復制的字典。

context = defaults.copy()
context.update(user)

這種方法與前一種區別不大。

對于本文所探討的問題,我更喜歡這種復制 defaults 字典的方法,可以很明顯地看出 defaults 字典代表了默認值。

得分:

  • 準確:是。
  • 符合語言習慣:是。

字典構造器

我們還可以將需要處理的字典傳入字典構造器( dict() ),這樣也能復制字典。

context = dict(defaults)
context.update(user)

此法與前一種非常相似, 但是沒有前一種直接明了(less explicit)。

得分:

  • 準確:是。
  • 符合語言習慣:一定程度上符合,不過我更喜歡前兩種方案。

關鍵詞參數hack(keywords hack)

你以前可能見過下面這個巧妙的解決方法:

context = dict(defaults, **user)

只有一行代碼,看上去很酷嘛。不過,這種解決方法有點難理解。

除了可讀性之外,還有一個更嚴重的問題:這種方案是錯的。

字典的鍵必須是字符串。在Python 2(解釋器是CPython)中,我們可以使用非字符串作為鍵,但別被蒙騙了:這種hack只是湊巧在使用標準CPython運行環境的Python 2中才有效。

得分:

  • 準確:否。沒有滿足第二點要求(鍵必須有效)
  • 符合語言習慣:否。這是一個hack。

字典解析(Dictionary comprehension)

我們嘗試下使用字典解析式來解決這個問題:

context = {k: v for d in [defaults, user] for k, v in d.items()}

成功了,但是可讀性有點差。

如果我們要處理未知數量的字典,這可能是種好方法,但是我們應該會想把字典解析式拆成多行,提高可讀性。在只處理兩個字典的情況下,這個雙嵌套(double nested)的解析式有點大材小用了。

得分:

  • 準確:是。
  • 符合語言習慣:可以認為不符合。

元素拼接(concatenate items)

假如我們從每個字典中獲取一個元素列表,將列表拼接起來,然后再利用拼接的列表在構建新字典?

context = dict(list(defaults.items()) + list(user.items()))

結果真的成功了。我們可以確定 user 字典中的鍵值會覆蓋掉 defaults 字典中的值,因為 user 字典的元素位于拼接列表的尾部。

在Python 2下,我們不需要先將字典轉換成列表,但是本文中我們使用的是Python 3(你也用的是Python 3,對吧?)。

得分:

  • 準確:是。
  • 符合語言習慣:不特別符合,代碼有些重復。

元素并集(union items)

在Python 3中,字典的items方法會返回一個dict_items對象,這是一個奇怪對象,居然支持并集操作。

context = dict(defaults.items() | user.items())

這種方案挺有意思。可惜并不準確。

首先,沒有滿足第一點要求( user 字典應該覆蓋 defaults )。因為兩個dict_items對象的并集是一個鍵值對(key-value pairs)的集合,而集合是無序的,所以重復鍵的處理方法無法預測。

另外,沒有滿足第三點要求(可以是任意的值),因為集合要求其中元素必須可哈希的,所以鍵-值元組中的鍵和值都必須是可哈希的才行。

得分:

  • 準確:否。沒有滿足第一點和第三點要求。
  • 符合語言習慣:否。

Chain items

目前為止,我們討論的解決方案中,最符合Python語言習慣而且又只有一行代碼的實現,是創建兩個items的列表,然后拼接并組成新字典。

我們可以使用 itertools.chain 來簡化items拼接的過程:

from itertools import chain
context = dict(chain(defaults.items(), user.items()))

這種方案效果不錯,可能比另外創建兩個不必要的列表更加高效。

得分:

準確:是。 符合語言習慣:比較符合,但是有點重復調用items方法。

ChainMap

ChainMap可以讓我們不用遍歷初始字典,就創建一個新字典:

from collections import ChainMap
context = ChainMap({}, user, defaults)

ChainMap將多個字典打包成一個proxy對象(一個“視圖”);ChainMap查找命令(譯者注:如context['name'])會檢索其中的字典,直到找到匹配的對象。

這里有幾個問題需要回答。

  1. 我們為什么把 user 放在 defaults 前面?

將參數按這樣的順序排列的目的,是為了確保滿足第一個點要求。ChainMap是按照順序檢索字典的,所以 user 會在 defaults 之前返回匹配的值。

  1. 為什么 user 之前有一個空字典?

這是為了滿足第五點要求。如果我們修改ChainMap對象,會影響到里面提供的第一個字典。我們不希望 user 發生變化,所以在前面放了一個空字典。

  1. 這樣真的會返回一個字典嗎?

ChainMap對象不是字典,而是類似字典的映射。如果我們的代碼中使用鴨子類型(duck typing),使用ChainMap是沒問題的,但是需要具體查看ChainMap的特性才能確定。此外,ChainMap對象與其底層的字典是相互勾連的,而且其刪除元素的方式也很有趣。

得分:

  • 準確:可能準確,需要考慮具體的用例。
  • 符合語言習慣:如果我們認為這種實現符合用例,那就是符合習慣的。

ChainMap轉換成字典(dict from ChainMap)

如果我們特別想要字典,可以將ChainMap轉換成字典:

context = dict(ChainMap(user, defaults))

需要注意的是,在其他解決方案中, user 一般出現在 defaults 之后;但是在這里卻相反。除了這點外,上面的代碼還是比較簡單,也明顯符合我們的要求。

得分:

  • 準確:是。
  • 符合語言習慣:是。

字典拼接(Dictionary concatenation)

我們能不能把兩個字典拼接起來呢?

context = defaults + user

這個想法很好,但可惜卻是不合法的。

得分:

  • 準確:否。無法執行。
  • 符合語言習慣:否。

字典拆分(Dictionary unpacking)

如果你在用Python 3.5,你可以使用一種全新的合并字典的方式(對虧了PEP 448):

context = {**defaults, **user}

這行代碼很簡潔,很Pythonic。里面有一些特殊符號,但是很明顯最后的結果至少是一個字典。

這段代碼在功能上與本文介紹的第一個方案是等價的:在第一個方案中,我們新建了一個空字典,然后依次往里面填充了來自 defaults 和 user 的元素。它滿足我們所有的要求,而且很可能是最簡單的一個解決方案。

得分:

  • 準確:是。
  • 符合語言習慣:是。

小結

在Python中有許多種合并字典的方法,但是能用一行代碼優雅地實現的方法并不多。

如果你使用Python 3.5,那么你應該這樣解決合并字典的問題:

context = {**defaults, **user}

如果你還沒有使用Python 3.5,建議你一一查看上面介紹的那些方法,確定哪一種最符合你的需求。

作者: Trey Hunner 譯者:EarlGrey

各種方案的性能比較如下:

multiple_update: 57 ms copy_and_update: 46 ms dict_constructor: 56 ms kwargs_hack: 45 ms dict_comprehension: 45 ms concatenate_items: 166 ms union_items: 163 ms chain_items: 122 ms chainmap: 86 ms dict_from_chainmap: 445 ms dict_unpacking: 27 ms

 本文由用戶 TraEMVU 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!