PEP 443 單分派泛型函數 — Python官方文檔譯文 [原創]
- 2020 年 3 月 31 日
- 筆記
PEP 443 — 單分派泛型函數(Single-dispatch generic functions)
英文原文:https://www.python.org/dev/peps/pep-0443
採集日期:2020-03-17
PEP: 443
Title: Single-dispatch generic functions
Author: Łukasz Langa [email protected]
Discussions-To: Python-Dev [email protected]
Status: Final
Type: Standards Track
Created: 22-May-2013
Post-History: 22-May-2013, 25-May-2013, 31-May-2013
Replaces: 245, 246, 3124
目錄
- 摘要
- 原由和目標(Rationale and Goals)
- 用戶 API(User API)
- 關於目前的實現代碼(Implementation Notes)
- 模板的用法(Usage Patterns)
- 替代方案(Alternative approaches)
- 致謝(Acknowledgements)
- 參考文獻(References)
- 版權(Copyright)
摘要(Abstract)
本 PEP 在 functools
標準庫模塊中提出了一種新機制,以提供一種簡單的泛型編程形式,名為單派發(single-dispatch)泛型函數。
泛型函數由多個函數組成,可為不同的類型實現相同的操作。調用期間應選用哪一實現由分派算法確定。如果實現代碼根據單個參數的類型做出選擇,則被稱為單派發。
原由和目標(Rationale and Goals)
Python 一直以內置和標準庫的形式提供了各種泛型函數,諸如 len()
、iter()
、pprint.pprint()
、copy.copy()
和 operator
模塊中的大部分函數。不過,目前情況是:
-
開發人員缺少一種簡單、直接的方式來新建泛型函數。
-
缺少一種將方法添加到現有泛型函數的標準方法,某些方法是用註冊函數添加的,另一些方法則需要定義
__special__
方法,且有可能是以動態替換(monkeypatching)的方式完成。
此外,為了決定該如何處理對象,而由 Python 代碼對收到的參數類型進行檢查,這種做法目前已經是一種常見的反面典型了(anti-pattern)。
比如,代碼可能既要能接受某類型的一個對象,又要能接受該類型對象組成的序列。目前,「淺顯的方案」是對類型進行檢查,但這種做法十分脆弱且無法擴展。
抽象基類(Abstract Base Class)能讓對象的當前行為發現起來更容易一些,但無助於增加新的行為。這樣採用現成(already-written)庫的開發人員可能就無法修改對象處理方式了,特別是當對象是由第三方創建的時候。
因此,本 PEP 提出了一種統一的 API,用裝飾符(decorator)來對動態重載(overload)進行定位。
用戶 API(User API)
若要定義泛型函數,請用 @singledispatch
裝飾器進行裝飾。注意分派將針對第一個參數的類型進行。創建函數的過程應如下所示:
>>> from functools import singledispatch >>> @singledispatch ... def fun(arg, verbose=False): ... if verbose: ... print("Let me just say,", end=" ") ... print(arg)
若要在函數中加入重載代碼,請使用泛型函數的 register()
屬性。這是一個裝飾器,接受一個類型參數,裝飾對象是針對該類型進行操作的函數:
>>> @fun.register(int) ... def _(arg, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> @fun.register(list) ... def _(arg, verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)
若要使用註冊 lambda 和已有函數,register()
屬性可以採用函數形式的用法:
>>> def nothing(arg, verbose=False): ... print("Nothing.") ... >>> fun.register(type(None), nothing)
register()
屬性將返回未經裝飾前的函數。這樣就能夠實現裝飾器的堆疊(stack)和序列化(pickle),以及為每個變量單獨創建單元測試過程:
>>> @fun.register(float) ... @fun.register(Decimal) ... def fun_num(arg, verbose=False): ... if verbose: ... print("Half of your number:", end=" ") ... print(arg / 2) ... >>> fun_num is fun False
泛型函數在被調用之後,會根據第一個參數的類型進行分派:
>>> fun("Hello, world.") Hello, world. >>> fun("test.", verbose=True) Let me just say, test. >>> fun(42, verbose=True) Strength in numbers, eh? 42 >>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True) Enumerate this: 0 spam 1 spam 2 eggs 3 spam >>> fun(None) Nothing. >>> fun(1.23) 0.615
如果沒有為某個類型註冊實現代碼,則會利用其方法解析順序查找更加通用的實現。用 @singledispatch
裝飾的原始函數已為 object
基類型做過註冊了,這意味着如果找不到更好的實現代碼,就會採用 object
的代碼。
若要檢測泛型函數針對某一給定類型會選用哪個實現代碼,請使用 dispatch()
屬性:
>>> fun.dispatch(float) <function fun_num at 0x104319058> >>> fun.dispatch(dict) # note: default implementation <function fun at 0x103fe0000>
若要訪問所有已註冊的實現代碼,請使用只讀的 registry
屬性:
>>> fun.registry.keys() dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>, <class 'decimal.Decimal'>, <class 'list'>, <class 'float'>]) >>> fun.registry[float] <function fun_num at 0x1035a2840> >>> fun.registry[object] <function fun at 0x103fe0000>
為了確保解釋和使用起來都很容易,並與 functools
模塊中的現有成員保持一致,故意只提供了這些 API,且必須如此(opinionate)。
關於目前的實現代碼(Implementation Notes)
本 PEP 介紹的功能已在 pkgutil
標準庫模塊中實現為 simplegeneric
。因為該部分實現代碼已較為成熟,所以多半是期望能保持不變。實現代碼可參考 hg.python.org。
用於分派的類型被設為裝飾器的參數。也曾考慮過另一種格式的函數註解,但最後還是拒絕納入。截至2013年5月,這種用法已經超出了標準庫的範疇,使用註解的最佳實踐尚存在爭議。
根據目前的 pkgutil.simplegeneric
實現代碼,遵照在抽象基類上註冊虛子類的約定,分派代碼的註冊過程將不是線程安全的。
抽象基類(Abstract Base Classes)
pkgutil.simplegeneric
的實現代碼依賴於多種形式的方法解析順序(method resolution order,MRO)。@singledispatch
會移除老式類和 Zope ExtensionClass 的特殊處理過程。更重要的是,它引入了對抽象基類(ABC)的支持。
在為 ABC 註冊泛型函數的實現代碼時,分派算法會切換為 C3 線性化(linearization)的擴展形式,這種形式會在給定參數的 MRO 中加入相關的 ABC。分派算法會在引入 ABC 功能的地方插入 ABC,即 issubclass(cls, abc)
針對類本身返回 True
,而針對其他所有的直接基類則返回 False
。在該類的 MRO 中,給定類的隱含 ABC(或是註冊的,或是通過 __len__()
等特殊方法推斷出來的)將直接插到最後一個顯式列出的 ABC 之後。
最簡單形式的線性化就是返回給定類型的 MRO:
>>> _compose_mro(dict, []) [<class 'dict'>, <class 'object'>]
如果第二個參數包含了給定類型的抽象基類,則基類會按可推算的順序插入:
>>> _compose_mro(dict, [Sized, MutableMapping, str, ... Sequence, Iterable]) [<class 'dict'>, <class 'collections.abc.MutableMapping'>, <class 'collections.abc.Mapping'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>, <class 'object'>]
儘管這種操作模式的速度會顯著降低,但所有分派決定都被緩存了下來。當要在泛型函數上註冊新的實現代碼時,或者用戶代碼在 ABC 上調用 register()
進行隱式子類化時,緩存將會失效。在後一種情況下,可能會造成一種含糊不清的分派狀況,例如:
>>> from collections import Iterable, Container >>> class P: ... pass >>> Iterable.register(P) <class '__main__.P'> >>> Container.register(P) <class '__main__.P'>
如果碰到這種含糊不清的狀況,@singledispatch
將拒絕做出猜測:
>>> @singledispatch ... def g(arg): ... return "base" ... >>> g.register(Iterable, lambda arg: "iterable") <function <lambda> at 0x108b49110> >>> g.register(Container, lambda arg: "container") <function <lambda> at 0x108b491c8> >>> g(P()) Traceback (most recent call last): ... RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'> or <class 'collections.abc.Iterable'>
請注意,如果在定義類時顯式給出了一個或多個 ABC 作為基類,則不會引發上述異常。這時將按 MRO 順序進行分派:
>>> class Ten(Iterable, Container): ... def __iter__(self): ... for i in range(10): ... yield i ... def __contains__(self, value): ... return value in range(10) ... >>> g(Ten()) 'iterable'
由 __len__()
或 __contains__()
這類特殊方法推斷出 ABC 的存在時,也會發生類似衝突:
>>> class Q: ... def __contains__(self, value): ... return False ... >>> issubclass(Q, Container) True >>> Iterable.register(Q) >>> g(Q()) Traceback (most recent call last): ... RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'> or <class 'collections.abc.Iterable'>
本 PEP 的早期版本中包含了一種更簡單的自定義處理方案,但那產生了很多結果詭異的邊界案例。
模板的用法(Usage Patterns)
本 PEP 建議只對特別標記為泛型的函數功能進行擴展。正如基類的方法可被子類覆蓋一樣,函數也可以被重載,以便為給定類型提供特定功能。
通用重載不等於任意重載,從某種意義上說,沒必要期望大家以不可推算的方式隨意對已有函數的功能進行重新定義。相反在通常情況下,實際的程序中用到的泛型函數更傾向於按照可推算模式進行,已註冊的實現代碼也應是非常容易發現的。
如果模塊要定義新的泛型操作,則通常還會在同一位置為現有類型實現所有必要的代碼。同樣,如果模塊要定義新的類型,則通常會在模塊中為所有已知或相關的泛型函數定義實現代碼。如此這般,不論是被重載函數,或是即將加入支持代碼的新類型,絕大多數已註冊的實現代碼都可以就近找到他們。
只有在極少數情況下,才會相關函數和類型之外的模塊中註冊實現代碼。在並非做不到或有意隱匿的情況下,極少數的實現代碼不在相關類型或函數附近,他們通常無需理解或知曉定義所在作用域之外的東西。(「支持模塊」除外,最佳實踐建議對他們作對應性的命名。)
如前所述,單派發泛型已在整個標準庫中大量應用。若有一種整潔、標準的實現方案,將為重構這些自定義的實現代碼指明一條通用的實現途徑,同時為適應用戶可擴展性打開了一扇大門。
替代方案(Alternative approaches)
在 PEP 3124 中,Phillip J. Eby 提出了一種成熟的解決方案,支持基於任意規則集的重載(已帶根據實參進行分派的默認實現),以及接口(interface)、適配(adaptation)和方法組合(combine)。PEAK 規則對 PJE 在 PEP 中描述的概念給出了參考實現。
這麼宏大的方案天生就是複雜的,很難讓大家形成共識。相反,本 PEP 僅專註於易於推斷的單個功能點。重點是要注意,本文並不排除目前或將來採用其他方法。
在 2005 年關於 Artima 的文章中,Guido van Rossum 提出了一種泛型函數的實現方案,支持依據函數的所有參數類型進行分派。同一方案也被 PyPI 中 Andrey Popp 的 generic
包和 David Mertz 的 gnosis.magic.multimethods
選用。
雖然猛一看似乎很不錯,但 Fredrik Lundh 的評論值得同意,即「如果設計 API 時要附帶一堆的邏輯,只是為了弄清楚函數應該執行的代碼,那可能就該另請高明了」。換句話說,本 PEP 中提出的單個參數方案不僅易於實現,而且清楚地表明更複雜的分派是一種反面典型。這裡的單參數分派還有一個優點,就是直接與面向對象編程中熟悉的方法分派機制相對應。唯一的區別就是,自定義的實現代碼與數據(面向對象的方法)緊密相關,或是與算法(單分派重載)更靠近。
PyPy 中的 RPython 提供了 extendabletype
,那是一個元類,使得類可以在外部進行擴展。結合 pairtype()
和 pair()
工廠方法,就能提供一種單派發泛型方案。
致謝(Acknowledgements)
除了 Phillip J. Eby 在 PEP 3124 和 PEAK-Rules 中的努力,本文還深受以下內容的影響:Paul Moore 建議將 pkgutil.simplegeneric
發佈到 functools
API 中去的原提案、Guido van Rossum 的多重方法文章、與 Raymond Hettinger 關於重寫通用 pprint 的多次討論。非常感謝 Nick Coghlan 鼓勵我創建此 PEP 並首先給出反饋。
參考文獻(References)
- PEP 8 在「編程建議」中標明「Python 標準庫將不使用函數註解,因為那會將某種註解風格過早確定下來」。
(https://www.python.org/dev/peps/pep-0008)
版權(Copyright)
本文已在公共領域發佈。