Python functools partial 混淆

4

请看下面的内容:

from functools import partial
def add(a, b, c):
   return 100 * a + 10 * b + c

add_part = partial(add, c = 2, b = 1)
add_part(3)
312

可以正常工作。但是:

def foo(x, y, z):
   return x+y+z

bar = partial(foo, y=3)

bar(1, 2)

抛出异常:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'y'

显然我错过了一些显而易见的东西,但是是什么呢?

你使用的是哪个版本的Python? - joseville
@joseville 我正在使用3.8版本 - 最近的版本有什么重大变化吗? - Igor Rivin
2个回答

5

partial() 的定义来自于官方的 functools 文档:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

这意味着在你的情况下,partial()返回的是foo()函数,并将其签名修改如下:
>>> from inspect import signature
>>> signature(bar)
<Signature (x, *, y=3, z)>

为了解决你的错误,你可以向bar()函数提供关键字参数:
def foo(x, y, z):
   return x+y+z

bar = partial(foo, y=3)

bar(x=1, z=2)

1
关于签名的好处很明显。由于在签名中*之后出现了z,我认为这意味着z是一个“仅限关键字参数”,而y也是一个“仅限关键字参数”,但它具有默认值。请参阅PEP 3102:https://www.python.org/dev/peps/pep-3102/ - joseville
我发现了一些“奇怪”的东西。也许你知道为什么:def baz(x, *, y=3, z): return x + y + zbar 有相同的签名(即两者都具有 <Signature (x, *, y=3, z)> 的签名)。然而,bar(1, 2) 导致 TypeError: foo() got multiple values for argument 'y',而 baz(1, 2) 也导致了一个 TypeError,但是错误信息不同:TypeError: bar() takes 1 positional argument but 2 were given。对我来说,第二个错误信息更符合 PEP 3102 的“函数调用行为”。我只是觉得很奇怪。 - joseville

2
Python 3.9文档中:
返回一个新的partial对象,当调用时,它会像使用位置参数args和关键字参数keywords调用func一样。如果调用时提供了更多的参数,则它们将被附加到args中。如果提供了额外的关键字参数,则它们会扩展并覆盖keywords。
def foo(x, y, z):
   return x+y+z

bar = partial(foo, y=3)

print(bar.args) # ()
print(bar.keywords) # {'y': 3}

当调用bar(1, 2)时,bar.args变为(1, 2),而bar.keywords仍然是{'y': 3}
注意到这一点后,下一步是参考PEP 3102中指定的"函数调用行为"
当调用一个函数时,输入参数会按照以下方式分配给形式参数: - 对于每个形式参数,都有一个插槽用于包含分配给该参数的参数值。 - 已经赋值的插槽被标记为“已填充”。尚未分配值的插槽被视为“空白”。 - 最初,所有插槽都被标记为空。 - 首先分配位置参数,然后是关键字参数。 - 对于每个位置参数: - 尝试将参数绑定到第一个未填充的参数插槽。如果插槽不是可变参数插槽,则将插槽标记为“已填充”。 - 如果下一个未填充的插槽是可变参数插槽,并且它没有名称,则会出现错误。 - 否则,如果下一个未填充的插槽是可变参数插槽,则所有剩余的非关键字参数都放置在可变参数插槽中。 - 对于每个关键字参数: - 如果存在与关键字相同名称的参数,则将参数值分配给该参数插槽。但是,如果参数插槽已经被填充,则会出现错误。 - 否则,如果存在“关键字字典”参数,则使用关键字名称作为字典键将参数添加到字典中,除非已经存在具有该键的条目,在这种情况下会出现错误。 - 否则,如果没有关键字字典和没有匹配的命名参数,则会出现错误。 - 最后: - 如果可变参数插槽尚未填充,则将一个空元组分配为其值。 - 对于每个剩余的空插槽:如果该插槽有默认值,则使用默认值填充插槽。如果没有默认值,则会出现错误。
“虽然上面的内容不确定是否最新,也不确定如何在这种情况下应用它,因为我不确定 'bar' 是否有位置参数或可变参数。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接