如何在Python中用partial填充特定的位置参数?

问题描述 投票:0回答:6

基本上,我想做的是:

>>> from functools import partial
>>> partial(str.startswith, prefix='a')('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: startswith() takes no keyword arguments

但更普遍的问题是,如何用

partial
填充特定的位置参数。

附注我确实意识到我可以使用

lambda
来代替。

python
6个回答
74
投票

这是做不到的。你必须制作一个包装函数。

表面上,您会使用关键字参数,正如您尝试做的那样 - 这就是它们的用途,对吗?不幸的是,正如您所发现的,Python 的标准库函数不接受命名参数。因此,考虑到当前的

partial
实现,如果不创建另一个函数来运行干扰,这是不可能的。

根据PEP 309的接受,接受包含的是“绑定leftmost位置参数和任何关键字的partial()类型构造函数”。此外,它还指出:

请注意,当对象像函数一样被调用时,位置参数将附加到提供给构造函数的参数,而关键字参数将覆盖和增强提供给构造函数的参数。

在创建对象和调用对象时可以提供位置参数、关键字参数或两者。

因为额外的位置参数是附加的,所以无法提供一些前面的位置参数(通过此实现)。

但是,事情还在继续:

部分应用来自的参数,或插入在任意位置的参数会产生自己的问题,但在发现良好的实现和不令人困惑的语义之前,我认为不应排除它。

因此,它显然可以放在桌面上,但就目前情况而言,它并没有以这种方式实现。

为了披露起见,上面引用的重点是我自己的。


33
投票

我知道这已经很旧了,但我一直遇到这个问题,而且我认为我已经创建了一个巧妙的解决方案。我使用稍微修改过的

partial
版本,如果您在构建对象时不知道它,则使用 Ellipses 对象 (
...
) 作为占位符值。这对于这个来说非常有用!

这是

__call__
的原始
partial

方法
def __call__(self, /, *args, **keywords):
    keywords = {**self.keywords, **keywords}
    return self.func(*self.args, *args, **keywords)

相反,我们可以使用文字

...
作为特殊情况来指示占位符值

>>> type(...)
<class 'ellipsis'>

这是整个实现:

class bind(partial):
    """
    An improved version of partial which accepts Ellipsis (...) as a placeholder
    """
    def __call__(self, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        iargs = iter(args)
        args = (next(iargs) if arg is ... else arg for arg in self.args)
        return self.func(*args, *iargs, **keywords)

使用方法相当简单

def foo(a, b, c, /, *, d):
    print(f"A({a}) B({b}) C({c}) D({d})")

f1 = bind(foo, 1, 2, 3, d=4)
f1()

f2 = bind(foo, 1, 2, d=4)
f2(3)

f3 = bind(foo, 1, ..., 3, d=4)
f3(2)

f4 = bind(foo, ..., 2, ..., d=4)
f4(1, 3)

f5 = bind(foo, ..., d=5)
f5(1, 2, 3, d=4)

14
投票

如果您确实需要这个,您可以使用 funcy 第三方库中的

rpartial

它的代码在这里:

def rpartial(func, *args):
    return lambda *a: func(*(a + args))

因此,您的案件可以按以下方式处理:

>>> startswith_a = rpartial(str.startswith, 'a')
>>> startswith_a('abc')
True
>>> startswith_a('def')
False

2
投票

使用此代码:

# See: functoolspartial for binding...

class Function(object):
    def __init__(self, fn):
        self.fn = fn

    def __call__(self, *args, **kwargs):
        return self.fn(*args, **kwargs)

    def __add__(self, other):
        def subfn(*args, **kwargs):
            return self(*args, **kwargs) + other(*args, **kwargs)
        return subfn


class arg(object):
    """Tagging class"""
    def __init__(self, index):
        self.index = index


class bind(Function):
    """Reorder positional arguments.
    Eg: g = f('yp', _1, 17, _0, dp=23)
    Then g('a', 'b', another=55) --> f('yp', 'b', 17, 'a', dp=23, another=55)
    """

    def __init__(self, fn, *pargs, **pkwargs):
        # Maximum index referred to by the user.
        # Inputs to f above this index will be passed through
        self.fn = fn
        self.pargs = pargs
        self.pkwargs = pkwargs
        self.max_gindex = max(
            (x.index if isinstance(x, arg) else -1 for x in pargs),
            default=-1)

    def __call__(self, *gargs, **gkwargs):
        fargs = \
            [gargs[x.index] if isinstance(x, arg) else x for x in self.pargs] + \
            list(gargs[self.max_gindex+1:])

        fkwargs = dict(self.pkwargs)
        fkwargs.update(gkwargs)    # Overwrite keys
        return self.fn(*fargs, *fkwargs)

然后尝试一下:

def myfn(a,b,c):
print(a,b,c)

bind(myfn, arg(1), 17, arg(0))(19,14)

0
投票

我的选择

def upartial(f, *args, **kwargs):
    """
    An upgraded version of partial which accepts not named parameters
    """
    params = f.__code__.co_varnames[1:]
    kwargs = {**{param: arg for param, arg in zip(params, args)}, **kwargs}
    return partial(f, **kwargs)

为自己努力

def my_func(a, b, c):
    return a + b + c

>>> upartial(my_func, "BB", c="CC")("AA")
'AABBCC'

0
投票

除了Goodies回答之外,使用占位符的另一个解决方案是(正如newtover尝试做的那样)仅通过相应的关键字传递位置参数。这可以通过以下代码实现:

from inspect import signature, Parameter, BoundArguments
from collections.abc import Callable


def partial_kw(func: Callable, **kwargs):
    sig = signature(func)
    pkwargs = kwargs
    
    def complete(*args, **kwargs):
        fkwargs = pkwargs | kwargs
        left_args_kw = [par.name for par in sig.parameters.values()
                   if par.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD, Parameter.VAR_POSITIONAL) 
                   and par.name not in fkwargs]
        arguments = dict(zip(left_args_kw, args)) | fkwargs
        ba = BoundArguments(sig, arguments)
        return func(*ba.args, **ba.kwargs)
    
    return complete()

示例:

partial_kw(tuple.__getitem__, key=1)((1, 2)) # returns 2

但我的解决方案似乎不适用于

str.startswith
,因为
signature
无法从函数中提取签名。

© www.soinside.com 2019 - 2024. All rights reserved.