PEP 309 – 部分函式應用
- 作者:
- Peter Harris <scav at blueyonder.co.uk>
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 建立日期:
- 2003年2月8日
- Python 版本:
- 2.5
- 釋出歷史:
- 2003年2月10日,2003年2月27日,2004年2月22日,2006年4月28日
注意
在接受此PEP之後,python-dev 和 comp.lang.python 上的進一步討論揭示了對幾個操作函式物件的工具的渴望,但這些工具與函數語言程式設計無關。與其為這些工具建立一個新模組,不如將“functional”模組重新命名為“functools”,以反映其新擴大的焦點,這一點得到了同意[1]。
此PEP中對“functional”模組的引用已保留,以供歷史參考。
摘要
本提案旨在提供一個函式或可呼叫類,允許從可呼叫物件和部分引數列表(包括位置引數和關鍵字引數)構造一個新的可呼叫物件。
我提議一個名為“functional”的標準庫模組,用於存放有用的高階函式,包括 partial() 的實現。
已將實現提交給 SourceForge [2]。
接受
補丁 #941881 於2005年被接受並應用於 Py2.5。它基本上與此處概述的一致,是一個 partial() 型別建構函式,繫結最左側的位置引數和任何關鍵字。partial 物件有三個只讀屬性 func、args 和 keywords。對 partial 物件的呼叫可以指定關鍵字,這些關鍵字會覆蓋物件本身的關鍵字。
關於是否透過 __get__ 方法修改 partial 實現以更密切地模擬等效函式的行為,目前存在單獨且持續的討論。
動機
在函數語言程式設計中,函式柯里化是一種透過單引數函式實現多引數函式的方式。一個N個引數的函式實際上是一個帶1個引數的函式,它返回另一個接受(N-1)個引數的函式。在 Haskell 和 ML 等語言中,函式應用的工作方式使得一個函式呼叫
f x y z
實際上意味著
(((f x) y) z)
這本來只是一個晦澀的理論問題,但實際上在程式設計中它卻非常有用。用部分應用引數到另一個函式來表達函式既優雅又強大,在函式式語言中被大量使用。
在某些函式式語言中(例如 Miranda),你可以使用像 (+1) 這樣的表示式來表示 Python 中 (lambda x: x + 1) 的等價物。
通常,這類語言是強型別的,因此編譯器總是知道預期的引數數量,並在給定函子且引數少於預期時能做正確的事情。
Python 不透過柯里化實現多引數函式,所以如果你想要一個部分應用引數的函式,你可能會像上面那樣使用 lambda,或者為每個例項定義一個命名函式。
然而,lambda 語法並非人人喜歡,至少可以說。此外,Python 靈活地使用位置引數和關鍵字引數傳遞引數,這為泛化部分應用的概念並實現 lambda 無法實現的功能提供了機會。
示例實現
這是在 Python 中建立帶有部分應用引數的可呼叫物件的一種方法。下面的實現基於 Scott David Daniels 提供的改進。
class partial(object):
def __init__(*args, **kw):
self = args[0]
self.fn, self.args, self.kw = (args[1], args[2:], kw)
def __call__(self, *args, **kw):
if kw and self.kw:
d = self.kw.copy()
d.update(kw)
else:
d = kw or self.kw
return self.fn(*(self.args + args), **d)
(一個類似的方案已經在 Python Cookbook 中存在了一段時間 [3]。)
請注意,當物件被當作函式呼叫時,位置引數會附加到提供給建構函式的引數之後,而關鍵字引數會覆蓋和擴充提供給建構函式的引數。
位置引數、關鍵字引數或兩者都可以在建立物件時和呼叫物件時提供。
使用示例
所以 partial(operator.add, 1) 有點像 (lambda x: 1 + x)。當然,這不是一個能體現其優勢的例子。
另外請注意,您也可以以同樣的方式包裝一個類,因為類本身就是物件的可呼叫工廠。所以在某些情況下,您可以透過部分應用引數到建構函式來特化類,而不是定義子類。
例如,partial(Tkinter.Label, fg='blue') 預設建立前景為藍色的 Tkinter 標籤。
這是一個簡單的例子,它使用部分應用來動態構造 Tkinter 小部件的回撥函式。
from Tkinter import Tk, Canvas, Button
import sys
from functional import partial
win = Tk()
c = Canvas(win,width=200,height=50)
c.pack()
for colour in sys.argv[1:]:
b = Button(win, text=colour,
command=partial(c.config, bg=colour))
b.pack(side='left')
win.mainloop()
廢棄的語法提案
我最初建議的語法是 fn@(*args, **kw),其含義與 partial(fn, *args, **kw) 相同。
在某些組合語言中,@ 符號用於表示暫存器間接定址,這裡的使用也是一種間接定址。f@(x) 不是 f(x),而是一個當你呼叫它時會變成 f(x) 的東西。
它沒有受到好評,所以我撤回了提案的這一部分。無論如何,@ 已經被用於新的裝飾器語法。
來自 comp.lang.python 和 python-dev 的反饋
其中表達的意見如下(我總結一下)
- Lambda 足夠好。
- @ 語法很醜(一致意見)。
- 它實際上是柯里化而不是閉包。在 ActiveState 的 Python Cookbook 上有一個幾乎相同的柯里化類實現。
- 一個柯里化類確實會是標準庫的一個有用補充。
- 它不是函式柯里化,而是部分應用。因此,現在建議的名稱是 partial()。
- 它可能沒有足夠有用以至於被內建。
- 一個名為
functional的模組的想法受到了好評,並且還有其他屬於那裡的東西(例如函式組合)。 - 為了完整起見,有人建議了另一個在函式呼叫中提供的引數之後追加部分引數的物件(可能稱為
rightcurry)。
我同意 lambda 通常足夠好,只是並非總是如此。而且我希望有可能進行有用的自省和子類化。
我不同意 @ 特別醜,但可能是我比較奇怪。我們有透過特殊標點符號明確區分的字典、列表和元組字面量——直接表達部分應用函式字面量並非不可能。然而,沒有一個人說他們喜歡它,所以對我來說它是一隻死鸚鵡。
我同意將該類命名為 partial 而不是 curry 或 closure,因此我已相應地修改了本 PEP 中的提案。但並非全部:一些對“curry”的不正確引用已保留,因為那是當時討論的焦點。
從右邊部分應用引數,或在任意位置插入引數會帶來它自己的問題,但在發現好的實現和不混淆的語義之前,我認為不應該排除它。
Carl Banks 釋出了一個作為真正函式式閉包的實現
def curry(fn, *cargs, **ckwargs):
def call_fn(*fargs, **fkwargs):
d = ckwargs.copy()
d.update(fkwargs)
return fn(*(cargs + fargs), **d)
return call_fn
他向我保證效率更高。
我還用 Pyrex 編寫了該類,以估計透過用 C 編寫它可能會如何提高效能
cdef class curry:
cdef object fn, args, kw
def __init__(self, fn, *args, **kw):
self.fn=fn
self.args=args
self.kw = kw
def __call__(self, *args, **kw):
if self.kw: # from Python Cookbook version
d = self.kw.copy()
d.update(kw)
else:
d=kw
return self.fn(*(self.args + args), **d)
與巢狀函式實現相比,Pyrex 的效能提升不到100%,因為為了完全通用,它必須透過 Python API 呼叫來操作。出於同樣的原因,C 實現不太可能快得多,因此用 C 編寫內建函式的理由不是很充分。
總結
我更希望標準庫中應提供某種部分應用函式和其他可呼叫物件的方法。
一個名為 functional 的標準庫模組應包含 partial 的實現,以及社群所需的任何其他高階函式。不過,其他可能屬於那裡的函式超出了本 PEP 的範圍。
實現、文件和單元測試的補丁(SF 補丁 931005、931007 和 931010)已提交但尚未提交。
Hye-Shik Chang 還提交了一個 C 實現,儘管預計在 Python 實現證明其足夠有用值得最佳化之後才會將其納入。
參考資料
版權
本文件已置於公共領域。
來源: https://github.com/python/peps/blob/main/peps/pep-0309.rst
最後修改: 2025-02-01 08:59:27 GMT