1. 程式人生 > >Python 3.10 的首個 PEP 誕生,內建型別 zip() 迎來新特性

Python 3.10 的首個 PEP 誕生,內建型別 zip() 迎來新特性

> 譯者前言:相信凡是用過 zip() 內建函式的人,都會贊同它很有用,但是,它的最大問題是可能會產生出非預期的結果。PEP-618 提出給它增加一個引數,可以有效地解決大家的痛點。 > > 這是 Python 3.10 版本正式採納的第一個 PEP,「Python貓」一直有跟進社群最新動態的習慣,所以翻譯了出來給大家嚐鮮,強烈推薦一讀。(PS:嚴格來說,zip() 是一個內建類(built-in type),而不是一個內建函式(built-in function),但我們一般都稱它為一個內建函式。) **PEP原文 :** [https://www.python.org/dev/peps/pep-0618/](https://www.python.org/dev/peps/pep-0618/) **PEP標題:** Add Optional Length-Checking To zip **PEP作者:** Brandt Bucher **建立日期:** 2020-05-01 **合入版本:** 3.10 **譯者** :[豌豆花下貓](https://zhuanlan.zhihu.com/pythonCat) @Python貓公眾號 **PEP翻譯計劃** :https://github.com/chinesehuazhou/peps-cn ## 摘要 本 PEP 建議給內建的 `zip` 新增一個可選的 strict 布林關鍵字引數。當啟用時,如果其中一個引數先被用盡了,則會引發 ValueError 。 ## 動機 從作者的個人經驗和一份[對標準庫的調查](https://www.python.org/dev/peps/pep-0618/%23examples#examples) 來看,明顯有很多(如果不是絕大多數)zip 用例要求可迭代物件必須是等長的。有時候,周圍程式碼的上下文可以保證這點,但是要 zip 處理的資料通常是由呼叫者傳入的、單獨提供的或者以某種方式生成的。在這些情況下,zip 的預設行為意味著錯誤的重構或邏輯錯誤,很容易悄悄地導致資料丟失。這些 bug 不僅難以定位,甚至難以被覺察到。 很容易想到造成這種問題的簡單案例。例如,以下程式碼在 items 為一個序列(sequence)時可以良好地執行,但是如果呼叫者將 item 重構為一個可消耗的迭代器,則程式碼會悄悄地產生縮短的、不匹配的結果: ```python def apply_calculations(items): transformed = transform(items) for i, t in zip(items, transformed): yield calculate(i, t) ``` zip 還有幾種常見用法。慣用的技巧性用法特別容易出問題,因為它們經常被不完全瞭解程式碼工作方式的使用者使用。下面是一個示例,解包到 zip 中以轉化成巢狀的可迭代物件: ```python >>> x = [[1, 2, 3], ["one" "two" "three"]] >>> xt = list(zip(*x)) ``` 另一個例子是將資料“分塊”成大小相等的組: ```python >>> n = 3 >>> x = range(n ** 2), >>> xn = list(zip(*[iter(x)] * n)) ``` 在第一個例子中,非矩形資料通常會導致邏輯錯誤。在第二個例子中,長度不是 n 的倍數的資料通常也是錯誤。因為這兩個習慣用法都會悄悄地忽略不匹配的尾部元素。 最有說服力的例子來自使用了 zip 的標準庫`ast` ,它在 literal_eval 裡產生過一個 bug,[會直接丟棄不匹配的節點](https://bugs.python.org/issue40355): ```python >>> from ast import Constant, Dict, literal_eval >>> nasty_dict = Dict(keys=[Constant(None)], values=[]) >>> literal_eval(nasty_dict) # Like eval("{None: }") {} ``` 實際上,筆者已經在 Python 的標準庫和工具中[找出了許多呼叫點,](https://www.python.org/dev/peps/pep-0618/%23examples#examples) 立即在這些位置啟用此新特性是恰當的。 ## 基本原理 一些評論者聲稱:布林開關常量是一種“程式碼壞氣味(code-smell)”,或者與 Python 的設計哲學背道而馳。 但是,Python 當前在內建函式上有幾個布林關鍵字引數的用法,它們通常使用編譯期常量來呼叫: - `compile(..., dont_inherit=True)` - `open(..., closefd=False)` - `print(..., flush=True)` - `sorted(..., reverse=True)` 標準庫中還有許多類似用法。 這個新引數的想法和名稱[最初是](https://mail.python.org/archives/list/python-ideas%40python.org/message/6GFUADSQ5JTF7W7OGWF7XF2NH2XUTUQM)由 Ram Rachum 提出的。該議題收到了 100 多個回覆,而候選的“equal”也獲得了相近的支援數。 筆者對它們沒有很強烈的偏好,儘管“equal equals” 讀起來有點尷尬。它還可能(錯誤地)暗示了 zip 的物件是相等的: ```python >>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True) ``` ## 規範 當用關鍵字引數 strict=True 呼叫內建類 zip 時,如果引數的長度不同,則生成的迭代器會引發 ValueError。這個異常就發生在迭代器正常停止迭代的地方。 ## 向上相容 此項更改是完全向上相容的。當前的 zip 不接受關鍵字引數,預設省略 strict 的“非嚴格”用法會保持不變。 ## 參考實現 筆者設計了一個[ C 實現](https://github.com/python/cpython/pull/20921)。 用 Python 大致翻譯如下: ```python def zip(*iterables, strict=False): if not iterables: return iterators = tuple(iter(iterable) for iterable in iterables) try: while True: items = [] for iterator in iterators: items.append(next(iterator)) yield tuple(items) except StopIteration: if not strict: return if items: i = len(items) plural = " " if i == 1 else "s 1-" msg = f"zip() argument {i+1} is shorter than argument{plural}{i}" raise ValueError(msg) sentinel = object() for i, iterator in enumerate(iterators[1:], 1): if next(iterator, sentinel) is not sentinel: plural = " " if i == 1 else "s 1-" msg = f"zip() argument {i+1} is longer than argument{plural}{i}" raise ValueError(msg) ``` ## 被拒絕的意見 ### (1)新增 itertools.zip_strict 這是 Python-Ideas 郵件列表上獲得最多支援的替代方案,因此值得在此處加以討論。它沒有任何嚴重的缺陷,如果本 PEP 被否絕,它是一個很好的替代。 雖然考慮到這一點,但是在 zip 中新增可選引數可以用較小的更改而更好地解決誘發此 PEP 的問題。 ### (2)依照先例 itertools 中有一個 zip_longest,這似乎讓人很有動機再新增一個 zip_strict。但是,zip_longest 在許多方面是一個更加複雜且特定的程式:它負責填寫缺失的值,但其它函式都不需要操心這種事。 如果 zip 和 zip_longest 同時放在 itertools 中,或者都作為內建函式,那麼在相同的地方新增 zip_strict 就確實是一個更有效的論點。然而,新的“strict”用法在介面和行為方面,相比起 zip_longest,更接近於 zip 的概念,但又不足以成為內建物件。考慮到這個原因,令 zip 就地擴展出一個新的選項,似乎是最自然的選擇。 ### (3)易用性 如果 zip 能夠防止此類 bug,那麼使用者在呼叫的地方啟動檢查,就會變得非常簡單。與其編寫一套繁重的邏輯來處理,不如用這個新特性來直接檢查。 有人還認為,在標準庫中放一個新的函式,相比在一個內建函式上加關鍵字引數,更“容易發現(discoverable)”。筆者不同意這一論斷。 ### (4)維護成本 儘管在提升易用性時,具體的實現是個次要問題,但重要的是要認識到,新增新的程式比修改原有程式複雜得多。與此 PEP 一起提供的 CPython 實現非常簡單,並且對 zip 的預設行為沒有顯著的效能影響,而在 itertools 中新增一個全新的程式將需要: - 複製 zip 的許多現有邏輯,zip_longest 就是這麼幹的。 - 大刀闊斧地重構 zip 或 zip_longest 或這兩者,以便共享一個公共的或者繼承性的實現(這可能會影響效能)。 ### (5)新增多個“模式”以供切換 如果預期有三個或更多模式(mode),這個建議才會比二元標誌更有意義。最顯而易見的三種模式是:“最短的”(當前 zip 的行為),“嚴格的”(本 PEP 提議的行為)和“最長的”(itertools.zip_longest 的行為)。 但是,除了當前的預設值以及本提案的“strict”模式,似乎不需要再新增其它模式。最可能的是新增一個“最長的”模式,但這需要一個新的 fillvalue 引數(它對於前兩種模式都沒有意義),另外,itertools.zip_longest 已經完美地處理了這種模式,若在 zip 中新增該模式,將會造成重複。目前尚不清楚哪一個是“顯而易見的”選擇:內建 zip 上的 mode 引數,還是已經長期存在於 itertools 中的 zip_longest。 ### (6)給 zip 新增方法或者建構函式 考慮以下兩個被提出來的做法: ```python >>> zm = zip(*iters).strict() >>> zd = zip.strict(*iters) ``` 尚不清楚哪個更好,或者哪個更差。如果 zip.strict 作為一個方法來實現,則 zm 沒問題,但是 zd 會出現幾種令人困惑的情況: - 返回不包裝在元組中的結果(如果 iters 僅包含一個元素,一個 zip 迭代器)。 - 引數型別錯誤時丟擲 TypeError(如果 iters 只包含一個元素,不是一個 zip 迭代器)。 - 否則,引數數量不對時丟擲 TypeError。 如果 zip.strict 是作為 classmethod 或 staticmethod 實現,則 zd 將成功執行,而 zm 將不產生任何結果(這正是我們最初要避免的問題)。 本提案還面臨著更為複雜的問題,因為 CPython 中 zip 內建類的實現細節是未文件化的。這意味著若選擇以上的某種行為,當前的實現就會被“鎖定”(或至少要求對其進行模擬)。 ### (7)變更 zip 的預設行為 zip 的預設行為沒有什麼“錯” ,因為在許多情況下,這確實是正確處理大小不等的輸入的方法。例如,在處理無限迭代器時,它非常有用。 itertools.zip_longest 已經用在仍然需要“額外”尾端資料的情況。 ### (8)使用回撥來處理剩餘物件 儘管基本上可以執行使用者需要的任何操作,但此解決方案在處理常見問題時(例如捨棄不匹配的長度),變得不必要的複雜且不直觀。 ### (9)引發一個 AssertionError 沒有內建函式或內建類的 API 會引發 AssertionError。此外,[官方文件](https://docs.python.org/3.9/library/exceptions.html%3Fhighlight%3Dassertionerror%23AssertionError#AssertionError) 這麼寫的(它的全部): > Raised when an `assert` statement fails. 由於此功能與 Python 的 assert 語句無關,因此不應該引發 AssertionError。使用者若希望在優化模式下禁用檢查(像一個 assert 語句),可以改用 strict = \_\_debug\_\_。 ### (10)在 map 上新增類似的特性 本 PEP 不建議對 map 作任何更改,因為很少使用帶有多個可迭代引數的map。但是,本 PEP 的裁定可作為將來討論類似特性的先例(應該出現)。 如果本 PEP 被拒絕,則 map 的那種特性實際上也不值得追求。如果通過了,則對 map 的更改不需要新的 PEP(儘管像所有提案一樣,都應仔細考慮其有用性)。為了保持一致性,它應遵循此處討論的跟 zip 相同的 API 和語義。 ### (11)什麼也不做 此建議可能最沒有吸引力。 悄悄地將資料截斷是一種特別令人討厭的 bug,而手寫一個健壯的解決方案卻[並非易事](https://stackoverflow.com/questions/32954486/zip-iterators-asserting-for-equal-length-in-python)。Python 自己的標準庫(前文提到的 ast)是有現實意義的反例,很容易就陷入本 PEP 試圖避免的那種陷阱。 **推薦閱讀:** 1、PEP中文翻譯計劃 (https://github.com/chinesehuazhou/peps-cn) 2、學習 Python,怎能不懂點PEP呢? (https://mp.weixin.qq.com/s/oRoBxZ2-IyuPOf_M