functools.partial我想知道这些和事实背后的故事 - 无论是声音设计还是继承的遗产 - inspect.signature(这里谈论 python 3.8)。
设置:
from functools import partial
from inspect import signature
def bar(a, b):
return a / b
Run Code Online (Sandbox Code Playgroud)
一切都从以下开始,这似乎符合咖喱标准。我们正在固定a位置3,a从签名中消失,它的值确实绑定到3:
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
Run Code Online (Sandbox Code Playgroud)
如果我们尝试为 指定一个替代值a,f不会告诉我们我们得到了一个意外的关键字,而是告诉我们它有多个参数值a:
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
Run Code Online (Sandbox Code Playgroud)
但现在如果我们b=3通过关键字修复,b则不会从签名中删除,它是对仅关键字的更改,我们仍然可以使用它(覆盖默认值,作为正常的默认值,这是我们在a之前无法做到的)案件):
f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
f(c=2, b=6) # TypeError: bar() got an unexpected keyword argument 'c'
Run Code Online (Sandbox Code Playgroud)
为什么会出现这样的不对称呢?
更奇怪的是,我们可以这样做:
f = partial(bar, b=3)
assert str(signature(f)) == '(a, *, b=3)'
assert f(6) == 2.0 == f(6, b=3)
assert f(6, b=1) == 6.0
Run Code Online (Sandbox Code Playgroud)
很好:对于仅关键字参数,默认分配给哪个参数不会令人困惑,但我仍然想知道这些选择背后的设计思维或约束是什么。
partial与位置参数一起使用f = partial(bar, 3)
Run Code Online (Sandbox Code Playgroud)
根据设计,在调用函数时,首先分配位置参数。那么从逻辑上讲,3应该分配给awith partial。将其从签名中删除是有意义的,因为无法再次为其分配任何内容!
当你有的时候f(a=2, b=6),你实际上是在做
bar(3, a=2, b=6)
Run Code Online (Sandbox Code Playgroud)
当你有的时候f(2, 2),你实际上是在做
bar (3, 2, 2)
Run Code Online (Sandbox Code Playgroud)
我们永远无法摆脱3
对于新的部分函数:
a用另一个位置参数给出不同的值a为其分配不同的值,因为它已经“填充”了如果存在与关键字同名的参数,则参数值将分配给该参数槽。然而,如果参数槽已经被填满,那就是一个错误。
我建议阅读pep-3102的函数调用行为部分,以更好地掌握这个问题。
partial与关键字参数一起使用f = partial(bar, b=3)
Run Code Online (Sandbox Code Playgroud)
这是一个不同的用例。我们正在将关键字参数应用于bar.
你正在功能性转向
def bar(a, b):
...
Run Code Online (Sandbox Code Playgroud)
进入
def f(a, *, b=3):
...
Run Code Online (Sandbox Code Playgroud)
其中b成为仅关键字参数而不是
def f(a, b=3):
...
Run Code Online (Sandbox Code Playgroud)
inspect.signature正确反映了 的设计决策partial。传递给的关键字参数partial旨在附加附加位置参数(source)。
请注意,此行为不一定会覆盖 提供的关键字参数f = partial(bar, b=3),即,b=3无论您是否提供第二个位置参数,都会应用此行为(TypeError如果您这样做,则会出现 a )。这与具有默认值的位置参数不同。
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given
Run Code Online (Sandbox Code Playgroud)
其中f(1, 2)相当于bar(1, 2, b=3)
覆盖它的唯一方法是使用关键字参数
>>> f(2, b=2)
Run Code Online (Sandbox Code Playgroud)
只能用关键字分配但按位置分配的参数?这是一个仅限关键字的参数。因此(a, *, b=3)代替(a, b=3).
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
Run Code Online (Sandbox Code Playgroud)
def bar(a=3, b)。a并被b如此称呼positional-or-keyword arguments。def bar(*, a=3, b)。a并且b是keyword-only arguments。尽管从语义上讲,a具有默认值,因此它是可选的,但我们不能将其保留为未分配状态b,因为positional-or-keyword argument如果我们想按位置使用,则需要为其分配一个值b。如果我们不提供 的值a,我们必须使用bas keyword argument。
将死!没有办法b成为positional-or-keyword argument我们想要的那样。
仅位置论证的 PEP也显示了其背后的基本原理。
这也和前面提到的“函数调用行为”有关系。
partial!= 柯里化和实现细节partial通过其实现包装原始函数,同时存储传递给它的固定参数。
它不是通过柯里化实现的。它是相当部分的应用,而不是函数式编程意义上的柯里化。partial本质上是首先应用固定参数,然后是使用包装器调用的参数:
def __call__(self, /, *args, **keywords):
keywords = {**self.keywords, **keywords}
return self.func(*self.args, *args, **keywords)
Run Code Online (Sandbox Code Playgroud)
这就解释了f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'。
另请参阅:为什么partial调用partial而不是curry
inspect检查的输出是另一个故事。
inspect它本身是一个产生用户友好输出的工具。partial()特别是(和partialmethod(),类似地),它遵循包装函数,同时考虑固定参数:
if isinstance(obj, functools.partial):
wrapped_sig = _get_signature_of(obj.func)
return _signature_get_partial(wrapped_sig, obj)
Run Code Online (Sandbox Code Playgroud)
请注意,我们的目标不是inspect.signature向您展示 AST 中包装函数的实际签名。
def _signature_get_partial(wrapped_sig, partial, extra_args=()):
"""Private helper to calculate how 'wrapped_sig' signature will
look like after applying a 'functools.partial' object (or alike)
on it.
"""
...
Run Code Online (Sandbox Code Playgroud)
所以我们有一个美好而理想的签名,f = partial(bar, 3)
但要f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'面对现实。
如果你非常想要柯里化,你如何在 Python 中实现它,以给你预期的方式TypeError?