天天看點

PEP 443 單分派泛型函數 -- Python官方文檔譯文 [原創]

PEP 443 單分派泛型函數 -- Python官方文檔譯文 [原創]

PEP 443 -- 單分派泛型函數(Single-dispatch generic functions)

目錄

摘要

原由和目标(Rationale and Goals)

使用者 API(User API)

關于目前的實作代碼(Implementation Notes)

抽象基類(Abstract Base Classes)

模闆的用法(Usage Patterns)

替代方案(Alternative approaches)

緻謝(Acknowledgements)

參考文獻(References)

版權(Copyright)

摘要(Abstract)

本 PEP 在 functools 标準庫子產品中提出了一種新機制,以提供一種簡單的泛型程式設計形式,名為單派發(single-dispatch)泛型函數。

泛型函數由多個函數組成,可為不同的類型實作相同的操作。調用期間應選用哪一實作由分派算法确定。如果實作代碼根據單個參數的類型做出選擇,則被稱為單派發。

Python 一直以内置和标準庫的形式提供了各種泛型函數,諸如 len()、iter()、pprint.pprint()、copy.copy() 和 operator 子產品中的大部分函數。不過,目前情況是:

開發人員缺少一種簡單、直接的方式來建立泛型函數。

缺少一種将方法添加到現有泛型函數的标準方法,某些方法是用注冊函數添加的,另一些方法則需要定義 __special__ 方法,且有可能是以動态替換(monkeypatching)的方式完成。

此外,為了決定該如何處理對象,而由 Python 代碼對收到的參數類型進行檢查,這種做法目前已經是一種常見的反面典型了(anti-pattern)。

比如,代碼可能既要能接受某類型的一個對象,又要能接受該類型對象組成的序列。目前,“淺顯的方案”是對類型進行檢查,但這種做法十分脆弱且無法擴充。

抽象基類(Abstract Base Class)能讓對象的目前行為發現起來更容易一些,但無助于增加新的行為。這樣采用現成(already-written)庫的開發人員可能就無法修改對象處理方式了,特别是當對象是由第三方建立的時候。

是以,本 PEP 提出了一種統一的 API,用裝飾符(decorator)來對動态重載(overload)進行定位。

若要定義泛型函數,請用 @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=" ")

...

@fun.register(list)

... 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):

... 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([, , ,

<class 'decimal.Decimal'>, <class 'list'>,
        <class 'float'>])           

fun.registry[float]

fun.registry[object]

為了確定解釋和使用起來都很容易,并與 functools 子產品中的現有成員保持一緻,故意隻提供了這些 API,且必須如此(opinionate)。

本 PEP 介紹的功能已在 pkgutil 标準庫子產品中實作為 simplegeneric。因為該部分實作代碼已較為成熟,是以多半是期望能保持不變。實作代碼可參考 hg.python.org。

用于分派的類型被設為裝飾器的參數。也曾考慮過另一種格式的函數注解,但最後還是拒絕納入。截至2013年5月,這種用法已經超出了标準庫的範疇,使用注解的最佳實踐尚存在争議。

根據目前的 pkgutil.simplegeneric 實作代碼,遵照在抽象基類上注冊虛子類的約定,分派代碼的注冊過程将不是線程安全的。

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 将拒絕做出猜測:

... 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())

本 PEP 的早期版本中包含了一種更簡單的自定義處理方案,但那産生了很多結果詭異的邊界案例。

本 PEP 建議隻對特别标記為泛型的函數功能進行擴充。正如基類的方法可被子類覆寫一樣,函數也可以被重載,以便為給定類型提供特定功能。

通用重載不等于任意重載,從某種意義上說,沒必要期望大家以不可推算的方式随意對已有函數的功能進行重新定義。相反在通常情況下,實際的程式中用到的泛型函數更傾向于按照可推算模式進行,已注冊的實作代碼也應是非常容易發現的。

如果子產品要定義新的泛型操作,則通常還會在同一位置為現有類型實作所有必要的代碼。同樣,如果子產品要定義新的類型,則通常會在子產品中為所有已知或相關的泛型函數定義實作代碼。如此這般,不論是被重載函數,或是即将加入支援代碼的新類型,絕大多數已注冊的實作代碼都可以就近找到他們。

隻有在極少數情況下,才會相關函數和類型之外的子產品中注冊實作代碼。在并非做不到或有意隐匿的情況下,極少數的實作代碼不在相關類型或函數附近,他們通常無需了解或知曉定義所在作用域之外的東西。(“支援子產品”除外,最佳實踐建議對他們作對應性的命名。)

如前所述,單派發泛型已在整個标準庫中大量應用。若有一種整潔、标準的實作方案,将為重構這些自定義的實作代碼指明一條通用的實作途徑,同時為适應使用者可擴充性打開了一扇大門。

在 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() 工廠方法,就能提供一種單派發泛型方案。

除了 Phillip J. Eby 在 PEP 3124 和 PEAK-Rules 中的努力,本文還深受以下内容的影響:Paul Moore 建議将 pkgutil.simplegeneric 釋出到 functools API 中去的原提案、Guido van Rossum 的多重方法文章、與 Raymond Hettinger 關于重寫通用 pprint 的多次讨論。非常感謝 Nick Coghlan 鼓勵我建立此 PEP 并首先給出回報。

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

英文原文:

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

本文已在公共領域釋出。

轉載位址

https://www.cnblogs.com/popapa/p/PEP443.html