“functools.partial”行为的基本原理

tho*_*len 6 python functools

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位置3a从签名中消失,它的值确实绑定到3

f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
Run Code Online (Sandbox Code Playgroud)

如果我们尝试为 指定一个替代值af不会告诉我们我们得到了一个意外的关键字,而是告诉我们它有多个参数值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)

很好:对于仅关键字参数,默认分配给哪个参数不会令人困惑,但我仍然想知道这些选择背后的设计思维或约束是什么。

PIG*_*208 5

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

对于新的部分函数:

  1. 我们不能a用另一个位置参数给出不同的值
  2. 我们不能使用关键字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)
  1. 你做不到def bar(a=3, b)a并被b如此称呼positional-or-keyword arguments
  2. 你可以做def bar(*, a=3, b)a并且bkeyword-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