我想知道这些 functools.partial
和 inspect.signature
事实背后的故事 - 无论是健全的设计还是继承的遗产 - (这里谈论 python 3.8)。
设置:
from functools import partial
from inspect import signature
def bar(a, b):
return a / b
一切都从以下开始,这似乎符合咖喱标准。
我们将 a
固定为 3
位置,a
从签名中消失,它的值确实绑定到 3
:
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
如果我们尝试为 a
指定替代值,f
不会告诉我们我们得到了意外的关键字,而是告诉我们它的参数有多个值 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'
但是现在如果我们修复b=3
通过关键字,b
不会从签名中删除,这是对仅关键字的一种更改,我们仍然可以使用它(覆盖默认值,作为正常的默认值,在前面的情况下我们无法使用 a
来做到这一点):
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
为什么会出现这样的不对称?
更奇怪的是,我们可以这样做:
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
很好:对于仅关键字参数,默认值分配给哪个参数不会造成混淆,但我仍然想知道这些选择背后的设计思维或约束是什么。
I'm wondering what the story -- whether sound design or inherited legacy -- is behind these functools.partial
and inspect.signature
facts (talking python 3.8 here).
Set up:
from functools import partial
from inspect import signature
def bar(a, b):
return a / b
All starts well with the following, which seems compliant with curry-standards.
We're fixing a
to 3
positionally, a
disappears from the signature and it's value is indeed bound to 3
:
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
If we try to specify an alternate value for a
, f
won't tell us that we got an unexpected keyword, but rather that it got multiple values for argument 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'
But now if we fix b=3
through a keyword, b
is not removed from the signature, it's kind changes to keyword-only, and we can still use it (overwrite the default, as a normal default, which we couldn't do with a
in the previous case):
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
Why such asymmetry?
It gets even stranger, we can do this:
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
Fine: For keyword-only arguments, there can be no confusing of what parameter a default is assigned to, but I still wonder what design-thinking or constraints are behind these choices.
发布评论
评论(2)
将
partial
与位置参数结合使用根据设计,在调用函数时,首先分配位置参数。那么从逻辑上讲,
3
应该用partial
分配给a
。将其从签名中删除是有意义的,因为无法再次为其分配任何内容!当你有
f(a=2, b=6)
时,你实际上在做当你有
f(2, 2)
时,你实际上在做我们永远不会摆脱
3
对于新的部分函数:
a
提供不同的值a
为它分配不同的值,因为它已经被“填充”我建议阅读函数调用行为部分pep-3102 来更好地掌握这个问题。
将
partial
与关键字参数结合使用这是一个不同的用例。我们将关键字参数应用于
bar
。您在功能上变成
了
b
成为仅关键字参数而不是
inspect.signature
正确地反映了partial
的设计决策。传递给partial
的关键字参数旨在附加额外的位置参数 (来源)。请注意,此行为不一定会覆盖由
f =partial(bar, b=3)
提供的关键字参数,即b=3
无论您是否提供第二个位置参数,都会应用(如果您这样做,将会出现TypeError
)。这与具有默认值的位置参数不同。其中
f(1, 2)
相当于bar(1, 2, b=3)
覆盖它的唯一方法是使用关键字参数
一个参数只能被分配一个关键字但位置?这是一个仅限关键字的参数。因此
(a, *, b=3)
而不是(a, b=3)
。非默认参数的基本原理遵循默认参数
def bar(a=3, b)
。a
和b
是所谓的位置或关键字参数
。a
和b
是仅关键字参数
。尽管从语义上讲,
a
有一个默认值,因此它是可选,但我们不能将其保留为未分配状态,因为b
是 <如果我们想按位置使用b
,则需要为 code>positional-or-keyword argument 分配一个值。如果我们不为a
提供值,则必须使用b
作为关键字参数
。将死!
b
不可能像我们预期的那样成为位置或关键字参数
。仅位置参数的 PEP 也很不错的显示了其背后的基本原理。
这也和前面提到的“函数调用行为”有关系。
partial
!= 柯里化 &实现细节partial
通过其实现来包装原始函数,同时存储您传递给它的固定参数。它不是通过柯里化实现的。它是部分应用,而不是函数式编程意义上的柯里化。
partial
本质上是首先应用固定参数,然后是使用包装器调用的参数:这解释了
f(a=2, b=6) # TypeError: bar() gets multiple values参数'a'
。另请参阅:为什么
partial
被称为partial
而不是curry
在
inspect
的背后Inspect 的输出是另一个故事。
inspect
本身是一个可以生成用户友好输出的工具。特别是对于partial()
(以及partialmethod()
,类似地),它遵循包装函数,同时考虑固定参数:请注意,它不是
inspect.signature
的目标是向您显示 AST 中包装函数的实际签名。所以我们有一个漂亮且理想的签名
f =partial(bar, 3)
但在现实中得到
f(a=2, b=6) # TypeError: bar() gets multiple values for argument 'a'
。后续
如果你非常想要柯里化,你如何在Python中实现它,以给你预期的
TypeError
?Using
partial
with a Positional ArgumentBy design, upon calling a function, positional arguments are assigned first. Then logically,
3
should be assigned toa
withpartial
. It makes sense to remove it from the signature as there is no way to assign anything to it again!when you have
f(a=2, b=6)
, you are actually doingwhen you have
f(2, 2)
, you are actually doingWe never get rid of
3
For the new partial function:
a
a different value with another positional argumenta
to assign a different value to it as it is already "filled"I recommend reading the function calling behavior section of pep-3102 to get a better grasp of this matter.
Using
partial
with a Keyword ArgumentThis is a different use case. We are applying a keyword argument to
bar
.You are functionally turning
into
where
b
becomes a keyword-only argumentinstead of
inspect.signature
correctly reflects a design decision ofpartial
. The keyword arguments passed topartial
are designed to append additional positional arguments (source).Note that this behavior does not necessarily override the keyword arguments supplied with
f = partial(bar, b=3)
, i.e.,b=3
will be applied regardless of whether you supply the second positional argument or not (and there will be aTypeError
if you do so). This is different from a positional argument with a default value.where
f(1, 2)
is equivalent tobar(1, 2, b=3)
The only way to override it is with a keyword argument
An argument that can only be assigned with a keyword but positionally? This is a keyword-only argument. Thus
(a, *, b=3)
instead of(a, b=3)
.The Rationale of Non-default Argument follows Default Argument
def bar(a=3, b)
.a
andb
are so calledpositional-or-keyword arguments
.def bar(*, a=3, b)
.a
andb
arekeyword-only arguments
.Even though semantically,
a
has a default value and thus it is optional, we can't leave it unassigned becauseb
, which is apositional-or-keyword argument
needs to be assigned a value if we want to useb
positionally. If we do not supply a value fora
, we have to useb
as akeyword argument
.Checkmate! There is no way for
b
to be apositional-or-keyword argument
as we intended.The PEP for positonal-only arguments also kind of shows the rationale behind it.
This also has something to do with the aforementioned "function calling behavior".
partial
!= Currying & Implementation Detailspartial
by its implementation wraps the original function while storing the fixed arguments you passed to it.IT IS NOT IMPLEMENTED WITH CURRYING. It is rather partial application instead of currying in the sense of functional programming.
partial
is essentially applying the fixed arguments first, then the arguments you called with the wrapper:This explains
f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
.See also: Why is
partial
calledpartial
instead ofcurry
Under the Hood of
inspect
The outputs of inspect is another story.
inspect
itself is a tool that produces user-friendly outputs. Forpartial()
in particular (andpartialmethod()
, similarly), it follows the wrapped function while taking the fixed parameters into account:Do note that it is not
inspect.signature
's goal to show you the actual signature of the wrapped function in the AST.So we have a nice and ideal signature for
f = partial(bar, 3)
but get
f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
in reality.Follow-up
If you want currying so badly, how do you implement it in Python, in the way which gives you the expected
TypeError
?当您向
partial
提供位置或关键字参数时,将构造新函数这其实和
partial
的思想是一致的,就是将参数传递给包装好的添加了传递给partial
的位置和关键字参数的函数这些情况的行为符合预期:
这种情况与上述情况不同,因为在前一种情况中,为
partial
提供了一个位置参数,而不是关键字争论。当位置参数提供给partial
时,将它们从签名中删除是有意义的。作为关键字提供的参数不会从签名中删除。到目前为止,不存在不一致或不对称的情况。
这里的签名是有意义的,并且是
partial(bar, a=3)
的期望——它的工作方式与def f(*, a=3, b): ...
是本例中的正确签名。请注意,在这种情况下,当您向partial
提供a=3
时,a
将成为仅关键字参数,b< 也是如此。 /代码>。
这是因为当位置参数作为关键字提供时,所有后续参数必须指定为关键字参数。
When you provide positional or keyword arguments to
partial
, the new function is constructedThis is actually consistent with the idea of
partial
, which is that arguments are passed to the wrapped function with the addition of positional and keyword arguments passed topartial
These cases behave as expected:
This case is different from the above because in the previous case, a positional argument was provided to
partial
, not a keyword argument. When positional arguments are provided topartial
, then it makes sense to remove them from the signature. Arguments provided as keywords are not removed from the signature.So far, there is no inconsistency or asymmetry.
The signature here makes sense and is the expectation for
partial(bar, a=3)
-- it works the same asdef f(*, a=3, b): ...
and is the correct signature in this case. Note that when you providea=3
topartial
in this case,a
becomes a keyword-only argument, as doesb
.This is because when a positional argument is provided as a keyword, all following arguments must be specified keyword arguments.