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)
- 抽象基類(Abstract Base Classes)
- 模板的用法(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)
- http://hg.python.org/features/pep-443/file/tip/Lib/functools.py#l359
- PEP 8 在“編程建議”中標明“Python 標準庫將不使用函式注解,因為那會將某種注解風格過早確定下來”,
(https://www.python.org/dev/peps/pep-0008)
- http://bugs.python.org/issue18244
- http://www.python.org/dev/peps/pep-3124/
- http://peak.telecommunity.com/DevCenter/PEAK_2dRules
- http://www.artima.com/weblogs/viewpost.jsp?thread=101605
- http://pypi.python.org/pypi/generic
- http://gnosis.cx/publish/programming/charming_python_b12.html(譯者注:鏈接已失效)
- https://bitbucket.org/pypy/pypy/raw/default/rpython/tool/pairtype.py
- http://bugs.python.org/issue5135
著作權(Copyright)
本文已在公共領域發布,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/162018.html
標籤:Python
上一篇:NoSQL概述
