<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Python新操作:字典合并操作符來了

          共 6678字,需瀏覽 14分鐘

           ·

          2020-03-02 23:22

          一、前言


          就在本周,字典合并特性(PEP 584[1])的提交被合入了 CPython 的主干分支,并在 2020-02-26 發(fā)布了 Python 3.9.0a4[2] 預覽版本。

          f77132a591f945606b873ad114bdff95.webp

          那什么是字典合并操作符呢?在回答這個問題前,我們不妨回憶下集合的合并操作。當我們想要對兩個結合做合并操作時,會怎么做呢?

          >>> s1 = {1, 2}
          >>> s2 = {2, 3}
          >>> s1 | s2 # s1 和 s2 取并集,生成新的集合;與 s1.union(s2) 等價
          {1, 2, 3}
          >>> s1 |= s2 # s1 和 s2 取并集,并更新到 s1 上;與 s1.update(s2) 等價
          >>> s1
          {1, 2, 3}

          類似地,我們希望 Python 中的字典能像集合一樣,使用 ||= 作為合并操作符,以解決我們在過去合并字典時感受到的“痛苦”,于是就有了 PEP 584

          今天就想和大家聊聊這個提案,不僅是要了解字典合并操作符的前世今生,更是要學習提案作者以及參與者是如何對引入一個新特性的思考,辯證性地分析利弊,最終確定引入。最后還想和大家分享下在 CPython 層面是如何實現(xiàn)的。


          二、背景

          在平時使用 Python 的過程中,我們有時會需要合并字典。目前合并字典有多種方式,它們或多或少都有些缺點。

          2.1 dict.update

          d1.update(d2) 確實能合并兩個字典,但它是在修改d1的基礎上進行。如果我們想要合并成一個新的字典,沒有一個直接使用表達式的方式,而需要借助臨時變量進行:

          e = d1.copy()
          e.update(d2)

          2.2 {**d1, **d2}

          字典解包可以將兩個字典合并為一個新的字典,但看起來有些丑陋,并且不能讓人顯而易見地看出這是在合并字典。

          {**d1, **d2} 還會忽略映射類型,并始終返回字典類型。

          2.3 collections.ChainMap

          ChainMap 很少有人知道,它也可以用作合并字典。但和前面合并方式相反,在合并兩個字典時,第一個字典的鍵會覆蓋第二個字典的相同鍵。

          此外,由于 ChainMap 是對入?yún)⒆值涞姆庋b,這意味著寫入 ChainMap 會修改原始字典:

          >>> from collections import ChainMap
          >>> d1 = {'a':1}
          >>> d2 = {'a':2}
          >>> merged = ChainMap(d1, d2)
          >>> merged['a'] # d1['a'] 會覆蓋 d2['a']
          1
          >>> merged['a'] = 3 # 實際等同于 d1['a'] = 3
          >>> d1
          {'a': 3}

          2.4 dict(d1, **d2)

          這是一種鮮為人知的合并字典的“巧妙方法”,但如果字典的鍵不是字符串,它就不能有效工作了:

          >>> d1 = {'a': 1}
          >>> d2 = {2: 2}
          >>> dict(d1, **d2)
          Traceback (most recent call last):
          ...
          TypeError: keywords must be strings

          三、原理

          新操作符同 dict.update 方法的關系,就和列表連接(+)、擴展(+=)操作符同 list.extend 方法的關系一樣。需要注意的是,這和集合中 |/|= 操作符同 set.update 的關系稍有不同。作者明確了允許就地運算符接受更廣泛的類型(就像 list 那樣)是一種更有用的設計,并且限制二進制操作符的操作數(shù)類型(就像 list 那樣)將有助于避免由復雜的隱式類型轉換引起的錯誤被吞掉。

          >>> l1 = [1, 2]
          >>> l1 + (3,) # 限制操作數(shù)的類型,不是列表就報錯
          Traceback (most recent call last)
          ...
          TypeError: can only concatenate list (not "tuple") to list
          >>> l1 += (3,) # 允許就地運算符接受更廣泛的類型(如元組)
          >>> l1
          [1, 2, 3]

          當合并字典發(fā)生鍵沖突時,以最右邊的值為準。這和現(xiàn)存的字典類似操作相符,比如:

          {'a': 1, 'a': 2} # 2 覆蓋 1
          {**d, **e} # e覆蓋d中相同鍵所對應的值
          d.update(e) # e覆蓋d中相同鍵所對應的值
          d[k] = v # v 覆蓋原有值
          {k: v for x in (d, e) for (k, v) in x.items()} # e覆蓋d中相同鍵所對應的值

          四、規(guī)范

          字典合并會返回一個新字典,該字典由左操作數(shù)與右操作數(shù)合并而成,每個操作數(shù)必須是 dict(或 dict 子類的實例)。如果兩個操作數(shù)中都出現(xiàn)一個鍵,則最后出現(xiàn)的值(即來自右側操作數(shù)的值)將會覆蓋:

          >>> d = {'spam': 1, 'eggs': 2, 'cheese': 3}
          >>> e = {'cheese': 'cheddar', 'aardvark': 'Ethel'}
          >>> d | e
          {'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}
          >>> e | d # 不符合交換律,左右互換操作數(shù)會得到不同的結果
          {'aardvark': 'Ethel', 'spam': 1, 'eggs': 2, 'cheese': 3}

          擴展賦值版本的就地操作:

          >>> d |= e # 將 e 更新到 d 中
          >>> d
          {'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}

          擴展賦值的行為和字典的 update 方法完全一樣,它還支持任何實現(xiàn)了映射協(xié)議(更確切地說是實現(xiàn)了 keys__getitem__ 方法)或鍵值對迭代對象。所以:

          >>> d | [('spam', 999)]   # “原理”章節(jié)中提到限制操作數(shù)的類型,不是字典或字典子類就報錯
          Traceback (most recent call last):
          ...
          TypeError: can only merge dict (not "list") to dict

          >>> d |= [('spam', 999)] # “原理”章節(jié)中提到允許就地運算符接受更廣泛的類型,其行為和 update 一樣,接受鍵值對迭代對象
          >>> d
          {'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel', 'spam': 999}

          五、主流觀點

          5.1 字典合并不符合交換律

          合并是符合交換律的,但是字典聯(lián)合卻沒有(d | e != e | d)。

          回應

          Python 中有過不符合交換律的合并先例:

          >>> {0} | {False}
          {0}
          >>> {False} | {0}
          {False}

          上述結果雖然是相等的,但是本質是不同的。通常來說,a | bb | a 并不相同。

          5.2 字典合并并不高效

          類似管道寫法使用多次字典合并并不高效,比如 d | e | f | g | h 會創(chuàng)建和銷毀三個臨時映射。

          回應

          這種問題在序列級聯(lián)時同樣會出現(xiàn)。

          序列級聯(lián)的每一次合并都會使序列中的元素總數(shù)增加,最終會帶來 O(N^2) 的性能開銷。而字典合并有可能會有重復鍵,因此臨時映射的大小并不會如此快速地增長。

          正如我們很少將大量的列表或元組連接在一起一樣,PEP的作者任務合并大量的字典也是少見情況。若是確實有這樣的訴求,那么最好使用顯式的循環(huán)和就地合并:

          new = {}
          for d in many_dicts:
          new |= d

          5.3 字典合并是有損的

          字典合并可能會丟失數(shù)據(jù)(相同鍵的值可能消失),其他形式的合并并不會。

          回應

          作者并不覺得這種有損是一個問題。此外,dict.update 也會發(fā)生這種情況,但并不會丟棄鍵,這其實是符合預期的。只不過是現(xiàn)在使用的不是 update 而是 |。

          如果從不可逆的角度考慮,其他類型的合并也是有損的。假設 a | b 的結果是365,那么 ab 是多少卻不得而知。

          5.4 只有一種方法達到目的

          字典合并不符合“Only One Way”的禪宗。

          回應

          其實并沒有這樣的禪宗?!癘nly One Way”起源于很早之前Perl社區(qū)對Python的誹謗。

          5.5 超過一種方法達到目的

          好吧,禪宗并沒有說“Only One Way To Do It”。但是它明確禁止“超過一種方法達到目的”。

          回應

          并沒有這樣的禁止。Python 之禪僅表達了對“僅一種顯而易見的方式”的偏愛。

          There should be one-- and preferably only one --obvious way to do
          it.

          它的重點是應該有一種明顯的方式達到目的。對于字典更新操作來說,我們可能希望至少執(zhí)行兩個不同的操作:

          • 就地更新字典:顯而易見的方式是使用 update() 方法。如果此提案被接受,|= 擴展賦值操作符也將等效,但這是擴展賦值如何定義的副作用。選擇哪種取決于使用者口味。
          • 合并兩個現(xiàn)存的字典到新字典中:此提案中顯而易見的方法是使用 | 合并操作符。

          實際上,Python 里經常違反對“僅一種方式”的偏愛。例如,每個 for 循環(huán)都可以重寫為 while 循環(huán);每個 if 塊都可以寫為 if/else 塊。列表、集合和字典推導都可以用生成器表達式代替。列表提供了不少于五種方法來實現(xiàn)級聯(lián):

          • 級聯(lián)操作符:a + b
          • 就地級聯(lián)操作符:a + = b
          • 切片分配:a[len(a):] = b
          • 序列解壓縮:[*a, *b]
          • 擴展方法:a.extend(b)

          我們不能太教條主義,不能因為它違反了“僅一種方式”就非常嚴格的拒絕有用的功能。

          5.6 字典合并讓代碼更難理解

          字典合并讓人們更難理解代碼的含義。為了解釋該異議,而不是具體引用任何人的話:“在看到 spam | eggs,如果不知道 spameggs 是什么,根本就不知道這個表達式的作用”。

          回應

          這確實如此,即使沒有該提案,| 操作符的現(xiàn)狀也是如此:

          • 對于 int/bool 是按位或
          • 對于 set/forzenset 是并集
          • 還可能是任何其他的重載操作

          添加字典合并看起來并不會讓理解代碼變得更困難。確定 spameggs 是映射類型并不比確定是集合還是整數(shù)要花更多的工作。其實良好的命名約定將會有助于改善情況:

          flags |= WRITEABLE  # 可能就是數(shù)字的按位或
          DO_NOT_RUN = WEEKENDS | HOLIDAYS # 可能就是集合合并
          settings = DEFAULT_SETTINGS | user_settings | workspace_settings # 可能就是字典合并

          5.7 參考下完整的集合API

          字典和集合很相似,應該要支持集合所支持的操作符:|、&^-。

          回應

          也許后續(xù)會有PEP來專門說明這些操作符如何用于字典。簡單來說:

          把集合的對稱差集(^)操作用在字典上面是顯而易見且自然。比如:

          >>> d1 = {"spam": 1, "eggs": 2}
          >>> d2 = {"ham": 3, "eggs": 4}

          對于 d1d2 對稱差集,我們期望 d1 ^ d2 應該是 {"spam": 1, "ham": 3}

          把集合的差集(-)操作用在字典上面也是顯而易見和自然的。比如 d1d2 的差集,我們期望:

          • d1 - d2{"spam": 1}
          • d2 - d1{"ham": 3}

          把集合的交集(&)操作用在字典上面就有些問題了。雖然很容易確定兩個字典中鍵的交集,但是如何處理鍵所對應的值就比較模糊。不難看出 d1d2 的共同鍵是 eggs,如果我們遵循“后者勝出”的一致性原則,那么值就是 4。

          六、已拒絕的觀點

          PEP 584 提案中羅列了很多已拒絕的觀點,比如使用 + 來合并字典;在合并字典時也合并值類型為列表的值等等。這些觀點都非常有意思,被拒絕的理由也同樣有說服力。限于篇幅的原因不再進一步展開,感興趣的可以閱讀 https://www.python.org/dev/peps/pep-0584/#id34。


          七、實現(xiàn)

          7.1 純 Python 實現(xiàn)

          def __or__(self, other):
          if not isinstance(other, dict):
          return NotImplemented
          new = dict(self)
          new.update(other)
          return new

          def __ror__(self, other):
          if not isinstance(other, dict):
          return NotImplemented
          new = dict(other)
          new.update(self)
          return new

          def __ior__(self, other):
          dict.update(self, other)
          return self

          純 Python 實現(xiàn)并不復雜,我們只需讓 dict 實現(xiàn)幾個魔法方法:

          • __or____ror__ 魔法方法對應于 | 操作符,__or__ 表示對象在操作符左側,__ror__ 表示對象在操作符右側。實現(xiàn)就是根據(jù)左側操作數(shù)生成一個新字典,再把右側操作數(shù)更新到新字典中,并返回新字典。
          • __ior__ 魔法方法對應于 |= 操作符,將右側操作數(shù)更新到自身即可。

          7.2 CPython 實現(xiàn)

          CPython 中字典合并的詳細實現(xiàn)可見此 PR:https://github.com/python/cpython/pull/12088/files 。

          最核心的實現(xiàn)如下:

          // 實現(xiàn)字典合并生成新字典的邏輯,對應于 | 操作符
          static PyObject *
          dict_or(PyObject *self, PyObject *other)
          {
          if (!PyDict_Check(self) || !PyDict_Check(other)) {
          Py_RETURN_NOTIMPLEMENTED;
          }
          PyObject *new = PyDict_Copy(self);
          if (new == NULL) {
          return NULL;
          }
          if (dict_update_arg(new, other)) {
          Py_DECREF(new); // 減少引用計數(shù)
          return NULL;
          }
          return new;
          }

          // 實現(xiàn)字典就地合并邏輯,對應于 |= 操作符
          static PyObject *
          dict_ior(PyObject *self, PyObject *other)
          {
          if (dict_update_arg(self, other)) {
          return NULL;
          }
          Py_INCREF(self); // 增加引用計數(shù)
          return self;
          }

          CPython 的實現(xiàn)邏輯和純Python實現(xiàn)幾乎一樣,唯獨需要注意的就是引用計數(shù)的問題,這關系到對象的垃圾回收。


          八、總結

          PEP 584是一個非常精彩的提案,引入 ||= 操作符用作字典合并,看似是一個比較簡單的功能,但所要考慮的情況卻不少。不僅需要說明這個提案的背景,目前有哪些方式可以達到目的,它們有哪些痛點;還要考慮對既有類型引入操作符所帶來的各種影響,對開發(fā)者提出的質疑和顧慮進行思考和解決。整個提案所涉及到的方法論、思考維度、知識點都非常值得學習。

          對使用者來說,合并字典將會變得更加方便。在提案的最后,作者給出了許多第三方庫在合并字典時采用新方式編寫的例子,可謂是簡潔了不少。詳見 https://www.python.org/dev/peps/pep-0584/#id50 。

          參考資料

          [1]

          PEP584: https://www.python.org/dev/peps/pep-0584/

          [2]

          Python 3.9.0a4: https://www.python.org/downloads/release/python-390a4/

          瀏覽 29
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费公开成人视频 | 在线日韩国产网站 | 波多野结衣不打码视频 | 日韩性爱视频在线观看 | 精品多人P群无码专区 |