首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >`functools.partial`行为的理论基础

`functools.partial`行为的理论基础
EN

Stack Overflow用户
提问于 2022-03-08 22:52:41
回答 2查看 431关注 0票数 6

我想知道这些functools.partialinspect.signature事实背后的故事--无论是合理的设计还是继承的遗产-- (这里讨论python3.8)。

设立:

代码语言:javascript
复制
from functools import partial
from inspect import signature

def bar(a, b):
    return a / b

所有的一切都从以下几个方面开始,这似乎符合咖喱标准。我们将a定位为3a从签名中消失,它的值确实绑定到3

代码语言:javascript
复制
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)

如果我们试图为a指定一个备用值,f将不会告诉我们我们得到了一个意外的关键字,而是它获得了参数a的多个值

代码语言:javascript
复制
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'

但是,如果我们通过关键字修复b=3,则b是从签名中删除的而不是,它是对关键字的类更改,而且我们仍然可以使用它(覆盖默认值,作为正常的默认设置,在前面的情况下a不能这样做):

代码语言:javascript
复制
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

为什么这么不对称?

更奇怪的是,我们可以做到:

代码语言:javascript
复制
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)'  # whaaa?! non-default argument follows default argument?

好的:对于只使用关键字的参数,不能混淆默认分配给什么参数,但我仍然想知道这些选择背后有什么设计思想或约束。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2022-03-09 02:21:52

使用带有位置参数的partial

代码语言:javascript
复制
f = partial(bar, 3)

根据设计,在调用函数时,首先分配位置参数。然后,在逻辑上,应该将3分配给带有partiala。从签名中删除它是有意义的,因为没有办法再给它分配任何东西!

当你拥有f(a=2, b=6)时,你实际上是在做

代码语言:javascript
复制
bar(3, a=2, b=6)

当你拥有f(2, 2)时,你实际上是在做

代码语言:javascript
复制
bar (3, 2, 2)

我们永远摆脱不了3

对于新的部分函数:

  1. 我们不能用另一个位置参数给a一个不同的值
  2. 我们不能使用关键字a给它分配一个不同的值,因为它已经“填充”了

如果有一个与关键字名称相同的参数,则将参数值分配给该参数槽。但是,如果参数槽已被填充,则这是一个错误。

为了更好地理解这一问题,我建议阅读佩普-3102佩普-3102部分。

使用带有关键字参数的partial

代码语言:javascript
复制
f = partial(bar, b=3)

这是一个不同的用例。我们将关键字参数应用于bar

你在功能上转向

代码语言:javascript
复制
def bar(a, b):
    ...

转到

代码语言:javascript
复制
def f(a, *, b=3):
    ...

其中,b变成了只使用关键字的参数,而不是

代码语言:javascript
复制
def f(a, b=3):
    ...

inspect.signature正确地反映了partial的设计决策。传递给partial的关键字参数被设计为附加其他位置参数(来源)。

请注意,此行为不一定覆盖f = partial(bar, b=3)提供的关键字参数,也就是说,无论是否提供第二个位置参数,都将应用b=3 (如果这样做,将有一个TypeError )。这与带有默认值的位置参数不同。

代码语言:javascript
复制
>>> f(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given

其中f(1, 2)等价于bar(1, 2, b=3)

重写它的唯一方法是使用关键字参数。

代码语言:javascript
复制
>>> f(2, b=2)

一个只能用关键字指定但按位置分配的参数?这是一个只使用关键字的参数。因此,(a, *, b=3)而不是(a, b=3)

非默认参数的基本原理遵循默认参数。

代码语言:javascript
复制
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)'  # whaaa?! non-default argument follows default argument?
  1. 你不能做def bar(a=3, b)ab被称为positional-or-keyword arguments
  2. 你可以做def bar(*, a=3, b)abkeyword-only arguments

尽管在语义上,a有一个默认值,因此它是可选的,但我们不能让它不被赋值,因为如果我们想要在位置上使用b,则需要为b (即positional-or-keyword argument )分配一个值。如果我们不为a提供一个值,就必须使用b作为keyword argument

去死吧!b不可能像我们所期望的那样成为positional-or-keyword argument

纯正电子参数的PEP也在某种程度上展示了其背后的原理。

这也与前面提到的“函数调用行为”有关。

partial !=运行和实现详细信息

partial通过其实现封装原始函数,同时存储传递给它的固定参数。

IT不是通过运行来实现的。相反,它是部分应用程序,而不是函数编程意义上的运行。partial首先应用固定参数,然后应用包装器调用的参数:

代码语言:javascript
复制
def __call__(self, /, *args, **keywords):
    keywords = {**self.keywords, **keywords}
    return self.func(*self.args, *args, **keywords)

这就解释了f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'

另见:curry

inspect的罩下

视察的输出是另一回事。

inspect本身是一个产生用户友好输出的工具。特别是对于partial() (和partialmethod(),类似地),它遵循包装函数,同时考虑到固定参数:

代码语言:javascript
复制
if isinstance(obj, functools.partial):
    wrapped_sig = _get_signature_of(obj.func)
    return _signature_get_partial(wrapped_sig, obj)

请注意,inspect.signature的目标不是在AST中显示包装函数的实际签名。

代码语言:javascript
复制
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.
    """
    ...

因此,我们为f = partial(bar, 3)提供了一个很好的、理想的签名,但在现实中得到了f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'

后续行动

如果您非常想要运行,那么如何在Python中实现它,从而给出预期的TypeError

票数 5
EN

Stack Overflow用户

发布于 2022-03-08 23:59:46

当您向partial提供位置或关键字参数时,将构造新函数。

F=部分(bar,3) f(a=2,b=6) # TypeError: bar()获得参数'a‘的多个值 f(c=2,b=6) # TypeError: bar()得到一个意外的关键字参数'c‘

这实际上与partial的思想是一致的,即通过添加传递给partial的位置和关键字参数将参数传递给包装函数。

这些案件的表现与预期相符:

代码语言:javascript
复制
bar(3, a=2, b=6)  # TypeError: bar() got multiple values for argument 'a'
bar(3, c=2, b=6)  # TypeError: bar() got an unexpected keyword argument 'c'

但是现在,如果我们通过a关键字修复b=3,则没有从签名中删除b, F=部分(bar,b=3) 断言str(签名(F)) == '(a,*,b=3)‘ 断言f(6) == 2.0 == f(6,b=3) 断言f(6,b=1) == 6.0

这种情况不同于上面的情况,因为在前一种情况中,位置参数是提供给partial的,而不是关键字参数。当向partial提供位置参数时,从签名中删除它们是有意义的。作为关键字提供的参数不会从签名中删除。

到目前为止,没有不一致或不对称。

F=部分(bar,a=3) 断言str(签名(F)) == '(*,a=3,b)‘# whaaa?!非默认参数跟随默认参数?

这里的签名是有意义的,也是partial(bar, a=3)的期望--它的工作原理与def f(*, a=3, b): ...一样,在本例中是正确的签名。请注意,在本例中将a=3提供给partial时,a成为只关键字的参数,b也是如此。

这是因为当位置参数作为关键字提供时,必须指定以下所有参数。

代码语言:javascript
复制
sig = signature(f)
sig.parameters['a'].kind  # <_ParameterKind.KEYWORD_ONLY: 3>
代码语言:javascript
复制
inspect.getfullargspec(f)
# FullArgSpec(args=[], varargs=None, varkw=None, defaults=None, kwonlyargs=['a', 'b'], kwonlydefaults={'a': 3}, annotations={})
票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71402387

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档