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