PEP 443 單分派泛型函式 -- Python官方文件譯文 [原創]
阿新 • • 發佈:2020-03-31
# PEP 443 -- 單分派泛型函式(Single-dispatch generic functions)
> 英文原文:[https://www.python.org/dev/peps/pep-0443](https://www.python.org/dev/peps/pep-0443/)
> 採集日期:2020-03-17
**PEP**: 443
**Title**: Single-dispatch generic functions
**Author**: Łukasz Langa
**Discussions-To**: Python-Dev
**Status**: Final
**Type**: Standards Track
**Created**: 22-May-2013
**Post-History**: 22-May-2013, 25-May-2013, 31-May-2013
**Replaces**: 245, 246, 3124
## 目錄
- [摘要](#abstract)
- [原由和目標(Rationale and Goals)](#rationale)
- [使用者 API(User API)](#api)
- [關於目前的實現程式碼(Implementation Notes)](#note)
* [抽象基類(Abstract Base Classes)](#abc)
- [模板的用法(Usage Patterns)](#pattern)
- [替代方案(Alternative approaches)](#alternative)
- [致謝(Acknowledgements)](#acknowledgements)
- [參考文獻(References)](#references)
- [版權(Copyright)](#copyright)
## 摘要(Abstract)
----
本 PEP 在 ``functools`` 標準庫模組中提出了一種新機制,以提供一種簡單的泛型程式設計形式,名為單派發(single-dispatch)泛型函式。
**泛型函式**由多個函式組成,可為不同的型別實現相同的操作。呼叫期間應選用哪一實現由分派演算法確定。如果實現程式碼根據單個引數的型別做出選擇,則被稱為**單派發**。
## 原由和目標(Rationale and Goals)
----
Python 一直以內建和標準庫的形式提供了各種泛型函式,諸如 ``len()``、``iter()``、``pprint.pprint()``、``copy.copy()`` 和 ``operator`` 模組中的大部分函式。不過,目前情況是:
1. 開發人員缺少一種簡單、直接的方式來新建泛型函式。
2. 缺少一種將方法新增到現有泛型函式的標準方法,某些方法是用註冊函式新增的,另一些方法則需要定義 ``__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)
>>> fun.dispatch(dict) # note: default implementation
````
若要訪問所有已註冊的實現程式碼,請使用只讀的 ``registry`` 屬性:
````
>>> fun.registry.keys()
dict_keys([, , ,
, ,
])
>>> fun.registry[float]
>>> fun.registry[object]
````
為了確保解釋和使用起來都很容易,並與 ``functools`` 模組中的現有成員保持一致,故意只提供了這些 API,且必須如此(opinionate)。
## 關於目前的實現程式碼(Implementation Notes)
----
本 PEP 介紹的功能已在 ``pkgutil`` 標準庫模組中實現為 ``simplegeneric``。因為該部分實現程式碼已較為成熟,所以多半是期望能保持不變。實現程式碼可參考 [hg.python.org](#ref-implementation)。
用於分派的型別被設為裝飾器的引數。也曾考慮過另一種格式的函式註解,但最後還是拒絕納入。截至2013年5月,這種用法已經超出了[標準庫](#ref-pep8)的範疇,使用註解的最佳實踐尚存在爭議。
根據目前的 ``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, [])
[, ]
````
如果第二個引數包含了給定型別的抽象基類,則基類會按可推算的順序插入:
````
>>> _compose_mro(dict, [Sized, MutableMapping, str,
... Sequence, Iterable])
[, ,
, ,
, ,
]
````
儘管這種操作模式的速度會顯著降低,但所有分派決定都被快取了下來。當要在泛型函式上註冊新的實現程式碼時,或者使用者程式碼在 ABC 上呼叫 ``register()`` 進行隱式子類化時,快取將會失效。在後一種情況下,可能會造成一種含糊不清的分派狀況,例如:
````
>>> from collections import Iterable, Container
>>> class P:
... pass
>>> Iterable.register(P)
>>> Container.register(P)
````
如果碰到這種含糊不清的狀況,``@singledispatch`` 將拒絕做出猜測:
````
>>> @singledispatch
... def g(arg):
... return "base"
...
>>> g.register(Iterable, lambda arg: "iterable")
at 0x108b49110>
>>> g.register(Container, lambda arg: "container")
at 0x108b491c8>
>>> g(P())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch:
or
````
請注意,如果在定義類時顯式給出了一個或多個 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:
or
````
本 PEP 的早期版本中包含了一種更簡單的自定義處理方案,但那產生了很多結果詭異的[邊界案例](#ref-issue18244)。
## 模板的用法(Usage Patterns)
----
本 PEP 建議只對特別標記為泛型的函式功能進行擴充套件。正如基類的方法可被子類覆蓋一樣,函式也可以被過載,以便為給定型別提供特定功能。
通用過載不等於*任意*過載,從某種意義上說,沒必要期望大家以不可推算的方式隨意對已有函式的功能進行重新定義。相反在通常情況下,實際的程式中用到的泛型函式更傾向於按照可推算模式進行,已註冊的實現程式碼也應是非常容易發現的。
如果模組要定義新的泛型操作,則通常還會在同一位置為現有型別實現所有必要的程式碼。同樣,如果模組要定義新的型別,則通常會在模組中為所有已知或相關的泛型函式定義實現程式碼。如此這般,不論是被過載函式,或是即將加入支援程式碼的新型別,絕大多數已註冊的實現程式碼都可以就近找到他們。
只有在極少數情況下,才會相關函式和型別之外的模組中註冊實現程式碼。在並非做不到或有意隱匿的情況下,極少數的實現程式碼不在相關型別或函式附近,他們通常無需理解或知曉定義所在作用域之外的東西。(“支援模組”除外,最佳實踐建議對他們作對應性的命名。)
如前所述,單派發泛型已在整個標準庫中大量應用。若有一種整潔、標準的實現方案,將為重構這些自定義的實現程式碼指明一條通用的實現途徑,同時為適應使用者可擴充套件性打開了一扇大門。
## 替代方案(Alternative approaches)
----
在 [PEP 3124](#ref-pep3124) 中,Phillip J. Eby 提出了一種成熟的解決方案,支援基於任意規則集的過載(已帶根據實參進行分派的預設實現),以及介面(interface)、適配(adaptation)和方法組合(combine)。[PEAK 規則](#ref-peak)對 PJE 在 PEP 中描述的概念給出了參考實現。
這麼巨集大的方案天生就是複雜的,很難讓大家形成共識。相反,本 PEP 僅專注於易於推斷的單個功能點。重點是要注意,本文並不排除目前或將來採用其他方法。
在 2005 年關於 [Artima](#ref-artima2005) 的文章中,Guido van Rossum 提出了一種泛型函式的實現方案,支援依據函式的所有引數型別進行分派。同一方案也被 [PyPI](#ref-pypi) 中 Andrey Popp 的 ``generic`` 包和 David Mertz 的 [``gnosis.magic.multimethods``](#ref-gnosis) 選用。
雖然猛一看似乎很不錯,但 Fredrik Lundh 的評論值得同意,即“如果設計 API 時要附帶一堆的邏輯,只是為了弄清楚函式應該執行的程式碼,那可能就該另請高明瞭”。換句話說,本 PEP 中提出的單個引數方案不僅易於實現,而且清楚地表明更復雜的分派是一種反面典型。這裡的單引數分派還有一個優點,就是直接與面向物件程式設計中熟悉的方法分派機制相對應。唯一的區別就是,自定義的實現程式碼與資料(面向物件的方法)緊密相關,或是與演算法(單分派過載)更靠近。
PyPy 中的 RPython 提供了 [``extendabletype``](#ref-pairtype),那是一個元類,使得類可以在外部進行擴充套件。結合 ``pairtype()`` 和 ``pair()`` 工廠方法,就能提供一種單派發泛型方案。
## 致謝(Acknowledgements)
----
除了 Phillip J. Eby 在 [PEP 3124](#ref-pep3124) 和 PEAK-Rules 中的努力,本文還深受以下內容的影響:Paul Moore 建議將 ``pkgutil.simplegeneric`` 釋出到 ``functools`` API 中去的[原提案](#ref-issue5135)、Guido van Rossum 的[多重方法](#ref-artima2005)文章、與 Raymond Hettinger 關於重寫通用 pprint 的多次討論。非常感謝 Nick Coghlan 鼓勵我建立此 PEP 並首先給出反饋。
## 參考文獻(References)
----
1. [http://hg.python.org/features/pep-443/file/tip/Lib/functools.py#l359](http://hg.python.org/features/pep-443/file/tip/Lib/functools.py#l359)
2. [PEP 8](https://www.python.org/dev/peps/pep-0008) 在“程式設計建議”中標明“Python 標準庫將不使用函式註解,因為那會將某種註解風格過早確定下來”。
([https://www.python.org/dev/peps/pep-0008](https://www.python.org/dev/peps/pep-0008))
3. [http://bugs.python.org/issue18244](http://bugs.python.org/issue18244)
4. [http://www.python.org/dev/peps/pep-3124/](http://www.python.org/dev/peps/pep-3124/)
5. [http://peak.telecommunity.com/DevCenter/PEAK_2dRules](http://peak.telecommunity.com/DevCenter/PEAK_2dRules)
6. [http://www.artima.com/weblogs/viewpost.jsp?thread=101605](http://www.artima.com/weblogs/viewpost.jsp?thread=101605)
7. [http://pypi.python.org/pypi/generic](http://pypi.python.org/pypi/generic)
8. [http://gnosis.cx/publish/programming/charming_python_b12.html](
http://gnosis.cx/publish/programming/charming_python_b12.html)(譯者注:連結已失效)
9. [https://bitbucket.org/pypy/pypy/raw/default/rpython/tool/pairtype.py](
https://bitbucket.org/pypy/pypy/raw/default/rpython/tool/pairtype.py)
10. [http://bugs.python.org/issue5135](http://bugs.python.org/issue5135)
## 版權(Copyright)
----
本文已在公共領域